diff --git a/Cargo.lock b/Cargo.lock index 2c0bb109..5b524185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anybuf" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a03eb4d55fa21466cac727930be07f82581223ee04bcaf61f934e98b7ecb859" + [[package]] name = "anyhow" version = "1.0.79" @@ -64,9 +76,27 @@ dependencies = [ "cw2 0.16.0", "schemars", "serde", - "sg-std", - "sg1", - "sg2", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 2.1.0", + "thiserror", +] + +[[package]] +name = "base-factory" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "schemars", + "serde", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg2 3.5.0", "thiserror", ] @@ -76,7 +106,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a1389609e84c7aac390051f3c06eed75aca1aa77709f1fab80c1c742909f393" dependencies = [ - "base-factory", + "base-factory 2.1.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.16.0", @@ -86,12 +116,37 @@ dependencies = [ "cw721-base 0.16.0", "schemars", "serde", - "sg-std", - "sg1", - "sg2", - "sg4", - "sg721", - "sg721-base", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 2.1.0", + "sg4 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", + "thiserror", + "url", +] + +[[package]] +name = "base-minter" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "base-factory 3.5.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "schemars", + "serde", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg4 3.5.0", + "sg721 3.5.0", + "sg721-base 3.5.0", "thiserror", "url", ] @@ -415,6 +470,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-controllers" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57de8d3761e46be863e3ac1eba8c8a976362a48c6abf240df1e26c3e421ee9e8" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-multi-test" version = "0.16.2" @@ -426,11 +496,31 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools", + "itertools 0.10.5", "k256 0.11.6", - "prost", + "prost 0.9.0", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-multi-test" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fff029689ae89127cf6d7655809a68d712f3edbdb9686c70b018ba438b26ca" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.12.1", + "prost 0.12.3", "schemars", "serde", + "sha2 0.10.8", "thiserror", ] @@ -575,6 +665,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cw4" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24754ff6e45f2a1c60adc409d9b2eb87666012c44021329141ffaab3388fccd2" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "schemars", + "serde", +] + [[package]] name = "cw4-group" version = "0.16.0" @@ -583,11 +686,29 @@ checksum = "dba1d15bff40b97bde05f36a0d44ac3369a085a4bda6c4673e33a9359513fdd2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-controllers", + "cw-controllers 0.16.0", "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", - "cw4", + "cw4 0.16.0", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4-group" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a22c3af54c52edf528673b420a67a1648be2c159b8ec778d2fbf543df24b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", "schemars", "serde", "thiserror", @@ -686,12 +807,6 @@ dependencies = [ "syn 1.0.107", ] -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.9.0" @@ -824,6 +939,18 @@ dependencies = [ "sha3", ] +[[package]] +name = "ethereum-verify" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "hex", + "sha2 0.10.8", + "sha3", +] + [[package]] name = "ff" version = "0.12.1" @@ -844,15 +971,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - [[package]] name = "form_urlencoded" version = "1.1.0" @@ -970,6 +1088,15 @@ 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.5" @@ -1031,9 +1158,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mockall" -version = "0.11.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" dependencies = [ "cfg-if", "downcast", @@ -1046,22 +1173,16 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.48", ] -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "num-traits" version = "0.2.15" @@ -1083,6 +1204,49 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open-edition-factory" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "base-factory 3.5.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "sg-metadata", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg721 3.5.0", + "thiserror", +] + +[[package]] +name = "open-edition-minter" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721-base 0.18.0", + "open-edition-factory", + "semver", + "serde", + "sg-metadata", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg4 3.5.0", + "sg721 3.5.0", + "thiserror", + "url", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1117,16 +1281,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "2.1.5" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" dependencies = [ - "difflib", - "float-cmp", - "itertools", - "normalize-line-endings", + "anstyle", "predicates-core", - "regex", ] [[package]] @@ -1170,7 +1330,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive 0.12.3", ] [[package]] @@ -1180,12 +1350,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.107", ] +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -1338,6 +1521,15 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "rs_merkle" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b241d2e59b74ef9e98d94c78c47623d04c8392abaf82014dfd372a16041128f" +dependencies = [ + "sha2 0.10.8", +] + [[package]] name = "rust_decimal" version = "1.28.1" @@ -1489,7 +1681,22 @@ dependencies = [ "cw-utils 0.16.0", "schemars", "serde", - "sg-std", + "sg-std 2.3.0", + "thiserror", +] + +[[package]] +name = "sg-controllers" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "schemars", + "serde", + "sg-std 3.2.0", "thiserror", ] @@ -1504,19 +1711,44 @@ dependencies = [ "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", - "ethereum-verify", + "ethereum-verify 2.1.0", "hex", "rust_decimal", "schemars", "serde", - "sg-std", - "sg-whitelist", - "sg1", + "sg-std 2.3.0", + "sg-whitelist 2.1.0", + "sg1 2.1.0", "sha2 0.10.8", "sha3", "thiserror", - "vending-minter", - "whitelist-immutable", + "vending-minter 2.1.0", + "whitelist-immutable 2.1.0", +] + +[[package]] +name = "sg-eth-airdrop" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "ethereum-verify 3.5.0", + "hex", + "rust_decimal", + "schemars", + "serde", + "sg-std 3.2.0", + "sg-whitelist 3.5.0", + "sg1 3.5.0", + "sha2 0.10.8", + "sha3", + "thiserror", + "vending-minter 3.5.0", + "whitelist-immutable 3.5.0", ] [[package]] @@ -1534,12 +1766,12 @@ dependencies = [ name = "sg-marketplace" version = "1.4.0" dependencies = [ - "base-minter", + "base-minter 2.1.0", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cute", - "cw-multi-test", + "cw-multi-test 0.16.2", "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", @@ -1549,17 +1781,17 @@ dependencies = [ "schemars", "semver", "serde", - "sg-controllers", - "sg-multi-test", - "sg-std", - "sg1", - "sg2", - "sg721", - "sg721-base", - "test-suite", + "sg-controllers 2.1.0", + "sg-multi-test 2.1.0", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", + "test-suite 2.1.0", "thiserror", - "vending-factory", - "vending-minter", + "vending-factory 2.1.0", + "vending-minter 2.1.0", ] [[package]] @@ -1579,22 +1811,22 @@ dependencies = [ "cw721-base 0.16.0", "schemars", "serde", - "sg-std", - "sg1", - "sg721", - "sg721-base", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", "stargaze-fair-burn", "thiserror", ] [[package]] name = "sg-marketplace-common" -version = "1.2.0" +version = "1.3.0" dependencies = [ - "base-minter", + "base-minter 3.5.0", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -1602,16 +1834,24 @@ dependencies = [ "cw721-base 0.18.0", "mockall", "serde", - "sg-multi-test", - "sg-std", - "sg1", - "sg721", - "sg721-base", + "sg1 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", "stargaze-fair-burn", - "test-suite", + "test-suite 3.5.0", "thiserror", ] +[[package]] +name = "sg-metadata" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "schemars", + "serde", +] + [[package]] name = "sg-multi-test" version = "2.1.0" @@ -1620,10 +1860,24 @@ checksum = "6b296a4ebb6a5e5eaec571f0820be1cd7eb9158597b41e7ee401828d7b60c0a2" dependencies = [ "anyhow", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.2", + "schemars", + "serde", + "sg-std 2.3.0", +] + +[[package]] +name = "sg-multi-test" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20744734b8049c64747bfb083bbc06a3c7204d1d34881ed3d89698e182aa9f97" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test 0.16.2", "schemars", "serde", - "sg-std", + "sg-std 3.2.0", ] [[package]] @@ -1634,15 +1888,34 @@ checksum = "dca6cb68acd389141d3109639fcdcc27477b97460106fea4d8b029430af23378" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-controllers", + "cw-controllers 0.16.0", "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", - "cw4", + "cw4 0.16.0", "schemars", "serde", - "sg-controllers", - "sg-std", + "sg-controllers 2.1.0", + "sg-std 2.3.0", + "thiserror", +] + +[[package]] +name = "sg-splits" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "schemars", + "serde", + "sg-controllers 3.5.0", + "sg-std 3.2.0", "thiserror", ] @@ -1661,6 +1934,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sg-std" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db53aebc2b4f981dc20a51213544adde8beaace6880345627f4babe2e1bc3ab" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw721 0.18.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "sg-whitelist" version = "2.1.0" @@ -1675,8 +1963,26 @@ dependencies = [ "rust_decimal", "schemars", "serde", - "sg-std", - "sg1", + "sg-std 2.3.0", + "sg1 2.1.0", + "thiserror", +] + +[[package]] +name = "sg-whitelist" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "rust_decimal", + "schemars", + "serde", + "sg-std 3.2.0", + "sg1 3.5.0", "thiserror", ] @@ -1689,7 +1995,20 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.16.0", "serde", - "sg-std", + "sg-std 2.3.0", + "thiserror", +] + +[[package]] +name = "sg1" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "anybuf", + "cosmwasm-std", + "cw-utils 1.0.3", + "serde", + "sg-std 3.2.0", "thiserror", ] @@ -1705,7 +2024,22 @@ dependencies = [ "cw-utils 0.16.0", "schemars", "serde", - "sg721", + "sg721 2.1.0", + "thiserror", +] + +[[package]] +name = "sg2" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "schemars", + "serde", + "sg721 3.5.0", "thiserror", ] @@ -1722,6 +2056,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sg4" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "sg721" version = "2.1.0" @@ -1736,6 +2082,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "sg721" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-utils 1.0.3", + "cw721-base 0.18.0", + "serde", + "thiserror", +] + +[[package]] +name = "sg721" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4363a35fe3a83f1170b1ed7ccdf2f79b7be73992c0a144eb8eb4ccfab38551c5" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-utils 1.0.3", + "cw721-base 0.18.0", + "serde", + "thiserror", +] + [[package]] name = "sg721-base" version = "2.1.0" @@ -1750,8 +2125,49 @@ dependencies = [ "cw721 0.16.0", "cw721-base 0.16.0", "serde", - "sg-std", - "sg721", + "sg-std 2.3.0", + "sg721 2.1.0", + "thiserror", + "url", +] + +[[package]] +name = "sg721-base" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "serde", + "sg-std 3.2.0", + "sg721 3.5.0", + "thiserror", + "url", +] + +[[package]] +name = "sg721-base" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ffedf506196cfb3b4840a98039d8ed880b9feeedb8ae208f70daa0fa27fc457" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "serde", + "sg-std 3.2.0", + "sg721 3.7.0", "thiserror", "url", ] @@ -1770,9 +2186,49 @@ dependencies = [ "cw721-base 0.16.0", "schemars", "serde", - "sg-std", - "sg721", - "sg721-base", + "sg-std 2.3.0", + "sg721 2.1.0", + "sg721-base 2.1.0", +] + +[[package]] +name = "sg721-nt" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "schemars", + "serde", + "sg-std 3.2.0", + "sg721 3.5.0", + "sg721-base 3.5.0", +] + +[[package]] +name = "sg721-updatable" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "schemars", + "semver", + "serde", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg721 3.5.0", + "sg721-base 3.5.0", + "thiserror", ] [[package]] @@ -1819,6 +2275,15 @@ dependencies = [ "rand", ] +[[package]] +name = "shuffle" +version = "0.1.7" +source = "git+https://github.com/webmaster128/shuffle?branch=rm-getrandom#2c267f41a590c85a327c9ffd8796cf06fa5cb9e9" +dependencies = [ + "bitvec", + "rand", +] + [[package]] name = "signature" version = "1.6.4" @@ -1873,7 +2338,7 @@ dependencies = [ "cw2 0.16.0", "schemars", "serde", - "sg-std", + "sg-std 2.3.0", "thiserror", ] @@ -1882,34 +2347,34 @@ name = "stargaze-marketplace-v2" version = "0.1.0" dependencies = [ "anyhow", - "base-minter", + "base-minter 3.5.0", "cosmwasm-schema", "cosmwasm-std", "cute", "cw-address-like", - "cw-multi-test", + "cw-multi-test 0.20.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "cw721 0.18.0", "cw721-base 0.18.0", + "digest 0.10.7", "semver", "serde", - "sg-controllers", + "sg-controllers 2.1.0", "sg-index-query", - "sg-marketplace-common 1.2.0", - "sg-multi-test", - "sg-std", - "sg1", - "sg2", - "sg721", - "sg721-base", - "stargaze-fair-burn", + "sg-marketplace-common 1.3.0", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 3.5.0", + "sg721 2.1.0", + "sg721-base 2.1.0", + "sha2 0.10.8", "stargaze-royalty-registry", - "test-suite", + "test-suite 3.5.0", "thiserror", - "vending-factory", - "vending-minter", + "vending-factory 3.5.0", + "vending-minter 3.5.0", ] [[package]] @@ -1919,7 +2384,7 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.2", "cw-storage-macro", "cw-storage-plus 0.16.0", "cw-utils 0.16.0", @@ -1930,23 +2395,22 @@ dependencies = [ "schemars", "serde", "sg-marketplace-common 1.1.0", - "sg-multi-test", - "sg-std", - "sg1", - "sg2", - "sg721", - "sg721-base", + "sg-multi-test 2.1.0", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", "stargaze-fair-burn", - "test-suite", + "test-suite 2.1.0", "thiserror", - "vending-minter", + "vending-minter 2.1.0", ] [[package]] name = "stargaze-royalty-registry" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552d49542e64baef99b84846bd787faeaf476afa0920676cb791a34a2be57772" +version = "0.4.0" +source = "git+https://github.com/public-awesome/core.git?rev=2b18f1fb294af6458fa7374a82204c47546c5615#2b18f1fb294af6458fa7374a82204c47546c5615" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1954,11 +2418,11 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.2", "schemars", + "semver", "serde", "sg-index-query", - "sg-std", - "sg721", - "sg721-base", + "sg721 3.7.0", + "sg721-base 3.7.0", "thiserror", ] @@ -2009,28 +2473,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3bdad2e4bd61b9e23b88f896e2a321b07198b036102fa1de1e143e25fb15f6" dependencies = [ "anyhow", - "base-factory", - "base-minter", + "base-factory 2.1.0", + "base-minter 2.1.0", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw4", - "cw4-group", + "cw-multi-test 0.16.2", + "cw4 0.16.0", + "cw4-group 0.16.0", "cw721 0.16.0", "cw721-base 0.16.0", - "sg-controllers", - "sg-eth-airdrop", - "sg-multi-test", - "sg-splits", - "sg-std", - "sg-whitelist", - "sg2", - "sg721", - "sg721-base", - "sg721-nt", - "vending-factory", - "vending-minter", - "whitelist-immutable", + "sg-controllers 2.1.0", + "sg-eth-airdrop 2.1.0", + "sg-multi-test 2.1.0", + "sg-splits 2.1.0", + "sg-std 2.3.0", + "sg-whitelist 2.1.0", + "sg2 2.1.0", + "sg721 2.1.0", + "sg721-base 2.1.0", + "sg721-nt 2.1.0", + "vending-factory 2.1.0", + "vending-minter 2.1.0", + "whitelist-immutable 2.1.0", +] + +[[package]] +name = "test-suite" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "anybuf", + "anyhow", + "base-factory 3.5.0", + "base-minter 3.5.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.20.0", + "cw-ownable", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "open-edition-factory", + "open-edition-minter", + "rs_merkle", + "serde", + "sg-controllers 3.5.0", + "sg-eth-airdrop 3.5.0", + "sg-metadata", + "sg-multi-test 3.1.0", + "sg-splits 3.5.0", + "sg-std 3.2.0", + "sg-whitelist 3.5.0", + "sg2 3.5.0", + "sg721 3.5.0", + "sg721-base 3.5.0", + "sg721-nt 3.5.0", + "sg721-updatable", + "vending-factory 3.5.0", + "vending-minter 3.5.0", + "vending-minter-merkle-wl", + "whitelist-immutable 3.5.0", + "whitelist-mtree", ] [[package]] @@ -2121,7 +2625,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b63afb31d2ed3b71a8271e6415f8bea3feeb9bc46977d446529c393d0621ef" dependencies = [ - "base-factory", + "base-factory 2.1.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.16.0", @@ -2129,10 +2633,30 @@ dependencies = [ "cw2 0.16.0", "schemars", "serde", - "sg-std", - "sg1", - "sg2", - "sg721", + "sg-std 2.3.0", + "sg1 2.1.0", + "sg2 2.1.0", + "sg721 2.1.0", + "thiserror", +] + +[[package]] +name = "vending-factory" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "base-factory 3.5.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "schemars", + "serde", + "sg-std 3.2.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg721 3.5.0", "thiserror", ] @@ -2154,17 +2678,78 @@ dependencies = [ "schemars", "semver", "serde", - "sg-std", - "sg-whitelist", - "sg1", - "sg2", - "sg4", - "sg721", + "sg-std 2.3.0", + "sg-whitelist 2.1.0", + "sg1 2.1.0", + "sg2 2.1.0", + "sg4 2.1.0", + "sg721 2.1.0", + "sha2 0.10.8", + "shuffle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror", + "url", + "vending-factory 2.1.0", +] + +[[package]] +name = "vending-minter" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "rand_core 0.6.4", + "rand_xoshiro", + "schemars", + "semver", + "serde", + "sg-std 3.2.0", + "sg-whitelist 3.5.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg4 3.5.0", + "sg721 3.5.0", + "sha2 0.10.8", + "shuffle 0.1.7 (git+https://github.com/webmaster128/shuffle?branch=rm-getrandom)", + "thiserror", + "url", + "vending-factory 3.5.0", +] + +[[package]] +name = "vending-minter-merkle-wl" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "rand_core 0.6.4", + "rand_xoshiro", + "schemars", + "semver", + "serde", + "sg-std 3.2.0", + "sg-whitelist 3.5.0", + "sg1 3.5.0", + "sg2 3.5.0", + "sg4 3.5.0", + "sg721 3.5.0", "sha2 0.10.8", - "shuffle", + "shuffle 0.1.7 (git+https://github.com/webmaster128/shuffle?branch=rm-getrandom)", "thiserror", "url", - "vending-factory", + "vending-factory 3.5.0", + "whitelist-mtree", ] [[package]] @@ -2187,17 +2772,57 @@ checksum = "6029b27989909842bba6886d213d4566d452b212aafd7a025197ed717f5abc8e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-controllers", + "cw-controllers 0.16.0", "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", "schemars", "serde", - "sg-std", - "sg-whitelist", + "sg-std 2.3.0", + "sg-whitelist 2.1.0", + "thiserror", +] + +[[package]] +name = "whitelist-immutable" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "schemars", + "serde", + "sg-std 3.2.0", + "sg-whitelist 3.5.0", "thiserror", ] +[[package]] +name = "whitelist-mtree" +version = "3.5.0" +source = "git+https://github.com/public-awesome/launchpad.git?rev=897d3ab057d381c1654c7246f3972ddd9f9238ce#897d3ab057d381c1654c7246f3972ddd9f9238ce" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "hex", + "rs_merkle", + "rust_decimal", + "schemars", + "serde", + "serde_json", + "sg-std 3.2.0", + "sg1 3.5.0", + "thiserror", + "url", +] + [[package]] name = "zeroize" version = "1.5.7" diff --git a/contracts/marketplace-v2/Cargo.toml b/contracts/marketplace-v2/Cargo.toml index 498cb4d1..ed5e83a9 100644 --- a/contracts/marketplace-v2/Cargo.toml +++ b/contracts/marketplace-v2/Cargo.toml @@ -39,28 +39,28 @@ cw2 = "1.1.2" cw721 = "0.18.0" cw721-base = { version = "0.18.0", features = ["library"] } -sg-marketplace-common = "1.2.0" +sg-marketplace-common = { path = "../../packages/sg-marketplace-common" } sg-index-query = "0.1.1" sg-std = "2.1.0" sg1 = "2.1.0" -sg2 = "2.1.0" +sg2 = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "sg2" } sg-controllers = "2.1.0" -stargaze-royalty-registry = { version = "0.3.0", features = ["library"] } -stargaze-fair-burn = { version = "1.0.4", features = ["library"] } +stargaze-royalty-registry = { git = "https://github.com/public-awesome/core.git", rev = "2b18f1fb294af6458fa7374a82204c47546c5615", package = "stargaze-royalty-registry" } sg721-base = { version = "2.1.0", features = ["library"] } sg721 = { version = "2.1.0", features = ["library"] } serde = "1.0.196" semver = "1.0.21" thiserror = "1.0.56" +sha2 = "0.10.0" +digest = "0.10.0" [dev-dependencies] -cw-multi-test = "0.16.2" -sg-multi-test = "2.1.0" -vending-factory = "2.1.0" -vending-minter = "2.1.0" -base-minter = "2.1.0" -test-suite = "2.1.0" +cw-multi-test = "0.20.0" +test-suite = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "test-suite" } +vending-factory = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "vending-factory" } +vending-minter = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "vending-minter" } +base-minter = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "base-minter" } cute = "0.3.0" anyhow = "1.0.79" diff --git a/contracts/marketplace-v2/src/error.rs b/contracts/marketplace-v2/src/error.rs index a71eb689..e3f2dd22 100644 --- a/contracts/marketplace-v2/src/error.rs +++ b/contracts/marketplace-v2/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, StdError}; +use cosmwasm_std::{OverflowError, StdError}; use cw_utils::PaymentError; use sg_marketplace_common::MarketplaceStdError; use stargaze_royalty_registry::ContractError as RoyaltyRegistryError; @@ -9,6 +9,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + #[error("{0}")] PaymentError(#[from] PaymentError), @@ -18,21 +21,12 @@ pub enum ContractError { #[error("{0}")] RoyaltyRegistryError(#[from] RoyaltyRegistryError), - #[error("InternalError: {0}")] - InternalError(String), - #[error("InvalidInput: {0}")] InvalidInput(String), - #[error("InsufficientFunds: expected {expected}")] - InsufficientFunds { expected: Coin }, + #[error("InsufficientFunds")] + InsufficientFunds, - #[error("EntityNotFound: {0}")] - EntityNotFound(String), - - #[error("EntityExists: {0}")] - EntityExists(String), - - #[error("EntityNotExpired: {0}")] - EntityNotExpired(String), + #[error("InternalError: {0}")] + InternalError(String), } diff --git a/contracts/marketplace-v2/src/events.rs b/contracts/marketplace-v2/src/events.rs index 2f4676a8..b4283ff4 100644 --- a/contracts/marketplace-v2/src/events.rs +++ b/contracts/marketplace-v2/src/events.rs @@ -1,115 +1,97 @@ -use crate::state::Ask; +use crate::{ + orders::{Ask, CollectionOffer, Offer}, + state::{AllowDenoms, Config}, +}; -use cosmwasm_std::Event; +use cosmwasm_std::{attr, Addr, Event}; use std::vec; -pub struct AskEvent<'a> { +pub struct ConfigEvent<'a> { pub ty: &'a str, - pub ask: &'a Ask, + pub config: &'a Config, } -impl<'a> From> for Event { - fn from(ae: AskEvent) -> Self { - Event::new(ae.ty.to_string()).add_attributes(ae.ask.get_event_attrs(vec![ - "collection", - "token_id", - "creator", - "price", - "asset_recipient", - "finders_fee_bps", - "expiration", - "removal_reward", - ])) +impl<'a> From> for Event { + fn from(ce: ConfigEvent) -> Self { + Event::new(ce.ty.to_string()).add_attributes(vec![ + attr("fee_manager", ce.config.fee_manager.to_string()), + attr("royalty_registry", ce.config.royalty_registry.to_string()), + attr("protocol_fee_bps", ce.config.protocol_fee_bps.to_string()), + attr( + "max_royalty_fee_bps", + ce.config.max_royalty_fee_bps.to_string(), + ), + attr("maker_reward_bps", ce.config.maker_reward_bps.to_string()), + attr("taker_reward_bps", ce.config.taker_reward_bps.to_string()), + ]) } } -// pub struct UpdatePairEvent<'a> { -// pub ty: &'a str, -// pub pair: &'a Pair, -// } - -// impl<'a> From> for Event { -// fn from(pe: UpdatePairEvent) -> Self { -// Event::new(pe.ty.to_string()).add_attributes(pe.pair.get_event_attrs(vec![ -// "pair_type", -// "swap_fee_percent", -// "reinvest_tokens", -// "reinvest_nfts", -// "bonding_curve", -// "spot_price", -// "delta", -// "is_active", -// "asset_recipient", -// ])) -// } -// } - -// pub struct NftTransferEvent<'a> { -// pub ty: &'a str, -// pub pair: &'a Pair, -// pub token_ids: &'a Vec, -// } - -// impl<'a> From> for Event { -// fn from(nte: NftTransferEvent) -> Self { -// Event::new(nte.ty.to_string()) -// .add_attributes(nte.pair.get_event_attrs(vec!["total_nfts"])) -// .add_attributes(nte.token_ids.iter().map(|token_id| ("token_id", token_id))) -// } -// } - -// pub struct TokenTransferEvent<'a> { -// pub ty: &'a str, -// pub funds: &'a Coin, -// } - -// impl<'a> From> for Event { -// fn from(tte: TokenTransferEvent) -> Self { -// Event::new(tte.ty.to_string()).add_attribute("funds", tte.funds.to_string()) -// } -// } +pub struct AllowDenomsEvent<'a> { + pub ty: &'a str, + pub allow_denoms: &'a AllowDenoms, +} -// pub struct SwapEvent<'a> { -// pub ty: &'a str, -// pub pair: &'a Pair, -// pub token_id: &'a str, -// pub sender_recipient: &'a Addr, -// pub quote_summary: &'a QuoteSummary, -// } +impl<'a> From> for Event { + fn from(ade: AllowDenomsEvent) -> Self { + let mut event = Event::new(ade.ty.to_string()); + + let enum_type = match &ade.allow_denoms { + AllowDenoms::Includes(_) => "includes", + AllowDenoms::Excludes(_) => "excludes", + }; + event = event.add_attribute("type", enum_type); + + match &ade.allow_denoms { + AllowDenoms::Includes(denoms) => { + for denom in denoms { + event = event.add_attribute("denom", denom); + } + } + AllowDenoms::Excludes(denoms) => { + for denom in denoms { + event = event.add_attribute("denom", denom); + } + } + } + + event + } +} -// impl<'a> From> for Event { -// fn from(se: SwapEvent) -> Self { -// let mut event = Event::new(se.ty.to_string()) -// .add_attributes(se.pair.get_event_attrs(vec!["spot_price", "is_active"])); +pub struct AskEvent<'a> { + pub ty: &'a str, + pub ask: &'a Ask, + pub attr_keys: Vec<&'a str>, +} -// event = event.add_attributes(vec![ -// attr("token_id", se.token_id), -// attr("sender_recipient", se.sender_recipient), -// attr("fair_burn_fee", se.quote_summary.fair_burn.amount), -// attr("seller_amount", se.quote_summary.seller_amount), -// ]); +impl<'a> From> for Event { + fn from(ae: AskEvent) -> Self { + Event::new(ae.ty.to_string()).add_attributes(ae.ask.get_event_attrs(ae.attr_keys)) + } +} -// if let Some(royalty) = se.quote_summary.royalty.as_ref() { -// event = event.add_attribute("royalty_fee", royalty.amount); -// } -// if let Some(swap) = se.quote_summary.swap.as_ref() { -// event = event.add_attribute("swap_fee", swap.amount); -// } +pub struct OfferEvent<'a> { + pub ty: &'a str, + pub offer: &'a Offer, + pub attr_keys: Vec<&'a str>, +} -// event -// } -// } +impl<'a> From> for Event { + fn from(oe: OfferEvent) -> Self { + Event::new(oe.ty.to_string()).add_attributes(oe.offer.get_event_attrs(oe.attr_keys)) + } +} -// pub struct PairInternalEvent<'a> { -// pub pair: &'a Pair, -// } +pub struct CollectionOfferEvent<'a> { + pub ty: &'a str, + pub collection_offer: &'a CollectionOffer, + pub attr_keys: Vec<&'a str>, +} -// impl<'a> From> for Event { -// fn from(pie: PairInternalEvent) -> Self { -// Event::new("pair-internal".to_string()).add_attributes(pie.pair.get_event_attrs(vec![ -// "total_tokens", -// "sell_to_pair_quote", -// "buy_from_pair_quote", -// ])) -// } -// } +impl<'a> From> for Event { + fn from(coe: CollectionOfferEvent) -> Self { + Event::new(coe.ty.to_string()) + .add_attributes(coe.collection_offer.get_event_attrs(coe.attr_keys)) + } +} diff --git a/contracts/marketplace-v2/src/execute.rs b/contracts/marketplace-v2/src/execute.rs index 85f1a24d..f27d2ad6 100644 --- a/contracts/marketplace-v2/src/execute.rs +++ b/contracts/marketplace-v2/src/execute.rs @@ -1,27 +1,23 @@ use crate::{ error::ContractError, - events::AskEvent, - helpers::{ - finalize_sale, only_order_creator, only_owner_or_seller, reconcile_funds, - validate_expiration_info, validate_order_options, validate_price, - }, - msg::{ExecuteMsg, OrderOptions, UpdateVal}, - orders::{MatchingOffer, RewardPayout}, + events::{AllowDenomsEvent, AskEvent, CollectionOfferEvent, ConfigEvent, OfferEvent}, + helpers::{finalize_sale, generate_id, only_contract_admin}, + msg::ExecuteMsg, + orders::{Ask, CollectionOffer, MatchingOffer, Offer, OrderDetails}, state::{ - asks, collection_offers, offers, Ask, CollectionOffer, ExpirationInfo, KeyString, Offer, - TokenId, SUDO_PARAMS, + asks, collection_offers, offers, AllowDenoms, Config, OrderId, TokenId, ALLOW_DENOMS, + CONFIG, NONCE, }, }; -use cosmwasm_std::{ensure, to_json_binary, Addr, Coin, DepsMut, Env, MessageInfo, WasmMsg}; -use cw_utils::{nonpayable, NativeBalance}; +use cosmwasm_std::{ensure, ensure_eq, Addr, DepsMut, Env, Event, MessageInfo, Response}; +use cw_utils::{maybe_addr, nonpayable, NativeBalance}; use sg_marketplace_common::{ - coin::transfer_coin, - nft::{only_owner, only_tradable, transfer_nft}, + coin::{transfer_coin, transfer_coins}, + nft::{only_owner, only_tradable, owner_of, transfer_nft}, + MarketplaceStdError, }; -use sg_std::Response; -use stargaze_fair_burn::msg::ExecuteMsg as FairBurnExecuteMsg; -use std::ops::AddAssign; +use std::ops::{Add, Sub}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; @@ -36,1045 +32,746 @@ pub fn execute( let api = deps.api; match msg { + ExecuteMsg::UpdateConfig { config } => { + execute_update_config(deps, env, info, config.str_to_addr(api)?) + } + ExecuteMsg::UpdateAllowDenoms { allow_denoms } => { + execute_update_allow_denoms(deps, env, info, allow_denoms) + } ExecuteMsg::SetAsk { collection, token_id, - price, - order_options, + details, } => execute_set_ask( deps, env, info, api.addr_validate(&collection)?, token_id, - price, - order_options.unwrap_or_default(), - ), - ExecuteMsg::UpdateAsk { - collection, - token_id, - asset_recipient, - finders_fee_bps, - expiration_info, - } => execute_update_ask( - deps, - env, - info, - api.addr_validate(&collection)?, - token_id, - asset_recipient, - finders_fee_bps, - expiration_info, + details.str_to_addr(api)?, ), + ExecuteMsg::UpdateAsk { id, details } => { + execute_update_ask(deps, env, info, id, details.str_to_addr(api)?) + } + ExecuteMsg::RemoveAsk { id } => execute_remove_ask(deps, env, info, id), ExecuteMsg::AcceptAsk { - collection, - token_id, - order_options, + id, + asset_recipient, + finder, } => execute_accept_ask( deps, env, info, - api.addr_validate(&collection)?, - token_id, - order_options.unwrap_or_default(), + id, + maybe_addr(api, asset_recipient)?, + maybe_addr(api, finder)?, ), - ExecuteMsg::RemoveAsk { - collection, - token_id, - } => execute_remove_ask(deps, info, api.addr_validate(&collection)?, token_id), - ExecuteMsg::RemoveExpiredAsk { - collection, - token_id, - } => execute_remove_expired_ask(deps, env, info, api.addr_validate(&collection)?, token_id), ExecuteMsg::SetOffer { collection, token_id, - price, - order_options, + details, } => execute_set_offer( deps, env, info, api.addr_validate(&collection)?, token_id, - price, - order_options.unwrap_or_default(), - ), - ExecuteMsg::UpdateOffer { - collection, - token_id, - asset_recipient, - finders_fee_bps, - expiration_info, - } => execute_update_offer( - deps, - env, - info, - api.addr_validate(&collection)?, - token_id, - asset_recipient, - finders_fee_bps, - expiration_info, + details.str_to_addr(api)?, ), + ExecuteMsg::UpdateOffer { id, details } => { + execute_update_offer(deps, env, info, id, details.str_to_addr(api)?) + } + ExecuteMsg::RemoveOffer { id } => execute_remove_offer(deps, env, info, id), ExecuteMsg::AcceptOffer { - collection, - token_id, - creator, - order_options, + id, + asset_recipient, + finder, } => execute_accept_offer( deps, env, info, - api.addr_validate(&collection)?, - token_id, - api.addr_validate(&creator)?, - order_options.unwrap_or_default(), - ), - ExecuteMsg::RemoveOffer { - collection, - token_id, - } => execute_remove_offer(deps, env, info, api.addr_validate(&collection)?, token_id), - ExecuteMsg::RejectOffer { - collection, - token_id, - creator, - } => execute_reject_offer( - deps, - env, - info, - api.addr_validate(&collection)?, - token_id, - api.addr_validate(&creator)?, - ), - ExecuteMsg::RemoveExpiredOffer { - collection, - token_id, - creator, - } => execute_remove_expired_offer( - deps, - env, - info, - api.addr_validate(&collection)?, - token_id, - api.addr_validate(&creator)?, + id, + maybe_addr(api, asset_recipient)?, + maybe_addr(api, finder)?, ), ExecuteMsg::SetCollectionOffer { collection, - price, - order_options, + details, } => execute_set_collection_offer( deps, env, info, api.addr_validate(&collection)?, - price, - order_options.unwrap_or_default(), - ), - ExecuteMsg::UpdateCollectionOffer { - collection, - asset_recipient, - finders_fee_bps, - expiration_info, - } => execute_update_collection_offer( - deps, - env, - info, - api.addr_validate(&collection)?, - asset_recipient, - finders_fee_bps, - expiration_info, + details.str_to_addr(api)?, ), + ExecuteMsg::UpdateCollectionOffer { id, details } => { + execute_update_collection_offer(deps, env, info, id, details.str_to_addr(api)?) + } + ExecuteMsg::RemoveCollectionOffer { id } => { + execute_remove_collection_offer(deps, env, info, id) + } ExecuteMsg::AcceptCollectionOffer { - collection, + id, token_id, - creator, - order_options, + asset_recipient, + finder, } => execute_accept_collection_offer( deps, env, info, - api.addr_validate(&collection)?, + id, token_id, - api.addr_validate(&creator)?, - order_options.unwrap_or_default(), - ), - ExecuteMsg::RemoveCollectionOffer { collection } => { - execute_remove_collection_offer(deps, env, info, api.addr_validate(&collection)?) - } - ExecuteMsg::RemoveExpiredCollectionOffer { - collection, - creator, - } => execute_remove_expired_collection_offer( - deps, - env, - info, - api.addr_validate(&collection)?, - api.addr_validate(&creator)?, + maybe_addr(api, asset_recipient)?, + maybe_addr(api, finder)?, ), } } -/// A creator may set an Ask on their NFT to list it on Marketplace -pub fn execute_set_ask( +pub fn execute_update_config( deps: DepsMut, env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, - price: Coin, - order_options_str: OrderOptions, + config: Config, ) -> Result { - only_owner(&deps.querier, &info, &collection, &token_id)?; - only_tradable(&deps.querier, &env.block, &collection)?; - validate_price(deps.storage, &price)?; + only_contract_admin(&deps.querier, &env, &info)?; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; + CONFIG.save(deps.storage, &config)?; - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); - - if !sudo_params.listing_fee.amount.is_zero() { - funds_due.add_assign(sudo_params.listing_fee.clone()); - response = response.add_message(WasmMsg::Execute { - contract_addr: sudo_params.fair_burn.to_string(), - msg: to_json_binary(&FairBurnExecuteMsg::FairBurn { recipient: None })?, - funds: vec![sudo_params.listing_fee.clone()], - }); - } - - let mut ask = Ask::new( - collection.clone(), - token_id.clone(), - price, - info.sender.clone(), - order_options.asset_recipient, - order_options.finders_fee_bps, - None, - ); - - // Ensure the ask does not exist - let ask_key = ask.key(); - ensure!( - asks().may_load(deps.storage, ask_key.clone())?.is_none(), - ContractError::EntityExists(format!("ask {}", &ask_key.to_string())) + let response = Response::new().add_event( + ConfigEvent { + ty: "set-config", + config: &config, + } + .into(), ); - // Check for a matching offer - let match_result = ask.match_with_offer(deps.as_ref(), &env); + Ok(response) +} - if let Ok(Some(matching_offer)) = match_result { - // If a match is found: - // * finalize the sale - // * remove the offer - response = finalize_sale( - deps.as_ref(), - &env, - &ask, - &matching_offer, - &sudo_params, - order_options.finder.as_ref(), - false, - response, - )?; +pub fn execute_update_allow_denoms( + deps: DepsMut, + env: Env, + info: MessageInfo, + allow_denoms: AllowDenoms, +) -> Result { + only_contract_admin(&deps.querier, &env, &info)?; - match matching_offer { - MatchingOffer::Offer(offer) => { - response = offer.remove(deps.storage, false, RewardPayout::Return, response)?; - } - MatchingOffer::CollectionOffer(collection_offer) => { - response = - collection_offer.remove(deps.storage, false, RewardPayout::Return, response)?; - } - } - } else { - // If no match is found, or there is an error in matching, continue creating the ask. - // Ask creation should: - // * escrow the nft - // * take removal fee if applicable - // * store the ask - // * emit 'set-ask' event - - // Escrow the NFT - response = transfer_nft( - &ask.collection, - &ask.token_id, - &env.contract.address, - response, - ); + ALLOW_DENOMS.save(deps.storage, &allow_denoms)?; - // Take removal fee if expiration is set - if let Some(expiration_info) = order_options.expiration_info { - funds_due.add_assign(expiration_info.removal_reward.clone()); - ask.order_info.expiration_info = Some(expiration_info); + let response = Response::new().add_event( + AllowDenomsEvent { + ty: "set-allow-denoms", + allow_denoms: &allow_denoms, } - - ask.save(deps.storage)?; - - response = response.add_event( - AskEvent { - ty: "set-ask", - ask: &ask, - } - .into(), - ) - } - - response = reconcile_funds(&info, funds_due, response)?; + .into(), + ); Ok(response) } -/// A creator may update the Ask on their NFT -pub fn execute_update_ask( +pub fn execute_set_ask( deps: DepsMut, env: Env, info: MessageInfo, collection: Addr, token_id: TokenId, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + details: OrderDetails, ) -> Result { - let api = deps.api; + nonpayable(&info)?; + only_owner(&deps.querier, &info, &collection, &token_id)?; + only_tradable(&deps.querier, &env.block, &collection)?; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + let ask = Ask::new(info.sender.clone(), collection, token_id, details); - let ask_key = Ask::build_key(&collection, &token_id); - let mut ask = asks() - .load(deps.storage, ask_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; + let config = CONFIG.load(deps.storage)?; + let response = ask.push_order(deps, &env, &config, Response::new())?; - only_order_creator(&info, &ask.order_info)?; + Ok(response) +} - // Update asset recipient if set - match asset_recipient { - Some(UpdateVal::Set(asset_recipient_val)) => { - ask.order_info.asset_recipient = Some(api.addr_validate(&asset_recipient_val)?); - } - Some(UpdateVal::Unset) => { - ask.order_info.asset_recipient = None; - } - None => {} - } +pub fn execute_update_ask( + deps: DepsMut, + _env: Env, + info: MessageInfo, + id: OrderId, + details: OrderDetails, +) -> Result { + nonpayable(&info)?; - // Update finders fee percent if set - match finders_fee_bps { - Some(UpdateVal::Set(finders_fee_bps_val)) => { - ask.order_info.finders_fee_bps = Some(finders_fee_bps_val); - } - Some(UpdateVal::Unset) => { - ask.order_info.finders_fee_bps = None; - } - None => {} - } + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - // Update the expiration on the ask if set - if let Some(expiration_info_update) = expiration_info { - // If a removal reward was paid, we need to refund it - if let Some(old_expiration_info) = ask.order_info.expiration_info { - if !old_expiration_info.removal_reward.amount.is_zero() { - response = transfer_coin( - old_expiration_info.removal_reward, - &ask.order_info.creator, - response, - ) - } - } + let mut ask = asks() + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("ask not found [{}]", id)))?; - match expiration_info_update { - UpdateVal::Set(new_expiration_info) => { - validate_expiration_info(&env.block, &sudo_params, &new_expiration_info)?; - funds_due.add_assign(new_expiration_info.removal_reward.clone()); - ask.order_info.expiration_info = Some(new_expiration_info); - } - UpdateVal::Unset => { - ask.order_info.expiration_info = None; - } - } - } + ensure_eq!( + info.sender, + ask.creator, + MarketplaceStdError::Unauthorized( + "only the creator of ask can perform this action".to_string() + ) + ); + ask.details = details; ask.save(deps.storage)?; - response = response.add_event( + let response = Response::new().add_event( AskEvent { ty: "update-ask", ask: &ask, + attr_keys: vec!["id", "price", "asset_recipient", "finder"], } .into(), ); - response = reconcile_funds(&info, funds_due, response)?; - Ok(response) } -/// Accepts an ask on a listed NFT (ie. Buy Now). -pub fn execute_accept_ask( +pub fn execute_remove_ask( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, - order_options_str: OrderOptions, + id: OrderId, ) -> Result { - only_tradable(&deps.querier, &env.block, &collection)?; - - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; - - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + nonpayable(&info)?; - let ask_key = Ask::build_key(&collection, &token_id); let ask = asks() - .load(deps.storage, ask_key) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("ask not found [{}]", id)))?; - funds_due.add_assign(ask.order_info.price.clone()); - - let offer = Offer::new( - collection, - token_id, - ask.order_info.price.clone(), - info.sender.clone(), - order_options.asset_recipient, - order_options.finders_fee_bps, - None, + ensure_eq!( + info.sender, + ask.creator, + MarketplaceStdError::Unauthorized( + "only the creator of ask can perform this action".to_string() + ) ); - response = finalize_sale( - deps.as_ref(), - &env, - &ask, - &MatchingOffer::Offer(offer.clone()), - &sudo_params, - order_options.finder.as_ref(), - true, - response, - )?; - - // Remove the ask - response = ask.remove(deps.storage, false, RewardPayout::Return, response)?; + let mut response = transfer_nft( + &ask.collection, + &ask.token_id, + &ask.asset_recipient(), + Response::new(), + ); - // If there is an existing offer remove it - let existing_offer_option = offers().may_load(deps.storage, offer.key())?; - if let Some(existing_offer) = existing_offer_option { - response = existing_offer.remove(deps.storage, true, RewardPayout::Return, response)?; - } + ask.remove(deps.storage)?; - response = reconcile_funds(&info, funds_due, response)?; + response = response.add_event(Event::new("remove-ask".to_string()).add_attribute("id", id)); Ok(response) } -/// Removes the ask on a particular NFT, only the creator can invoke this operation. -pub fn execute_remove_ask( +pub fn execute_accept_ask( deps: DepsMut, + env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, + id: OrderId, + asset_recipient: Option, + finder: Option, ) -> Result { - nonpayable(&info)?; - - let ask_key = Ask::build_key(&collection, &token_id); - let ask = asks().load(deps.storage, ask_key)?; - - only_order_creator(&info, &ask.order_info)?; + let mut funds = NativeBalance(info.funds.clone()); + funds.normalize(); - let response = ask.remove(deps.storage, true, RewardPayout::Return, Response::new())?; - - Ok(response) -} + let ask = asks() + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("ask not found [{}]", id)))?; -/// Operation that anyone can invoke to remove an expired ask on a particular NFT. -pub fn execute_remove_expired_ask( - deps: DepsMut, - env: Env, - info: MessageInfo, - collection: Addr, - token_id: TokenId, -) -> Result { - nonpayable(&info)?; + funds = funds + .sub(ask.details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; - let ask_key = Ask::build_key(&collection, &token_id); - let ask = asks().load(deps.storage, ask_key.clone())?; + let nonce = NONCE.load(deps.storage)?.wrapping_add(1); + NONCE.save(deps.storage, &nonce)?; - ensure!( - ask.order_info.is_expired(&env.block), - ContractError::EntityNotExpired(format!("ask {}", ask_key.to_string())) + let offer = Offer::new( + info.sender.clone(), + ask.collection.clone(), + ask.token_id.clone(), + OrderDetails { + price: ask.details.price.clone(), + asset_recipient: asset_recipient.clone(), + finder: finder.clone(), + }, + env.block.height, + nonce, ); - let response = ask.remove( - deps.storage, + let config = CONFIG.load(deps.storage)?; + let mut response = finalize_sale( + deps, + &env, + &ask, + &config, + &MatchingOffer::Offer(offer), true, - RewardPayout::Other(info.sender), Response::new(), )?; + // Transfer remaining funds back to user + if !funds.is_empty() { + response = transfer_coins(funds.into_vec(), &info.sender, response); + } + Ok(response) } -/// Places an offer on a listed or unlisted NFT. The offer is escrowed in the contract. pub fn execute_set_offer( deps: DepsMut, env: Env, info: MessageInfo, collection: Addr, token_id: TokenId, - price: Coin, - order_options_str: OrderOptions, + details: OrderDetails, ) -> Result { only_tradable(&deps.querier, &env.block, &collection)?; - validate_price(deps.storage, &price)?; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + let mut funds = NativeBalance(info.funds.clone()); + funds.normalize(); + + let nonce = NONCE.load(deps.storage)?.wrapping_add(1); + NONCE.save(deps.storage, &nonce)?; - let mut offer = Offer::new( - collection.clone(), - token_id.clone(), - price, + let offer = Offer::new( info.sender.clone(), - order_options.asset_recipient, - order_options.finders_fee_bps, - None, + collection, + token_id, + details, + env.block.height, + nonce, ); - // Ensure the offer does not exist - ensure!( - offers().may_load(deps.storage, offer.key())?.is_none(), - ContractError::EntityExists(format!("offer {}", offer.key().to_string())) - ); + let matching_ask = offer.match_with_ask(deps.as_ref())?; - let matching_ask = offer.match_with_ask(deps.as_ref(), &env)?; + let mut response = Response::new(); if let Some(ask) = matching_ask { - // If a matching ask is found: - // * ensure at least ask price is paid - // * perform the sale - // * remove the ask - funds_due.add_assign(ask.order_info.price.clone()); + // If a matching ask is found perform the sale + funds = funds + .sub(ask.details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; + let config: Config = CONFIG.load(deps.storage)?; response = finalize_sale( - deps.as_ref(), + deps, &env, &ask, + &config, &MatchingOffer::Offer(offer), - &sudo_params, - order_options.finder.as_ref(), true, response, )?; - - response = ask.remove(deps.storage, false, RewardPayout::Return, response)?; } else { // If no match is found. Offer creation should: - // * ensure offer price is paid - // * take removal fee if applicable // * store the offer - // * emit event and hook - funds_due.add_assign(offer.order_info.price.clone()); + // * emit event - if let Some(expiration_info) = order_options.expiration_info { - funds_due.add_assign(expiration_info.removal_reward.clone()); - offer.order_info.expiration_info = Some(expiration_info); - } + funds = funds + .sub(offer.details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; + + offer.save(deps.storage)?; - offers().save(deps.storage, offer.key(), &offer)?; + response = response.add_event( + OfferEvent { + ty: "set-offer", + offer: &offer, + attr_keys: vec![ + "id", + "creator", + "collection", + "token_id", + "price", + "asset_recipient", + "finder", + ], + } + .into(), + ) } - response = reconcile_funds(&info, funds_due, response)?; + // Transfer remaining funds back to user + if !funds.is_empty() { + response = transfer_coins(funds.into_vec(), &info.sender, response); + } Ok(response) } -/// A buyer may update their offer pub fn execute_update_offer( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + id: OrderId, + details: OrderDetails, ) -> Result { - let api = deps.api; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - let offer_key = Offer::build_key(&collection, &token_id, &info.sender); let mut offer = offers() - .load(deps.storage, offer_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("offer not found [{}]", id)))?; - // Update asset recipient if set - match asset_recipient { - Some(UpdateVal::Set(asset_recipient_val)) => { - offer.order_info.asset_recipient = Some(api.addr_validate(&asset_recipient_val)?); - } - Some(UpdateVal::Unset) => { - offer.order_info.asset_recipient = None; - } - None => {} - } + ensure_eq!( + info.sender, + offer.creator, + MarketplaceStdError::Unauthorized( + "only the creator of offer can perform this action".to_string() + ) + ); - // Update finders fee percent if set - match finders_fee_bps { - Some(UpdateVal::Set(finders_fee_bps_val)) => { - offer.order_info.finders_fee_bps = Some(finders_fee_bps_val); - } - Some(UpdateVal::Unset) => { - offer.order_info.finders_fee_bps = None; - } - None => {} - } + let mut funds = NativeBalance(info.funds.clone()); + funds.normalize(); - // Update the expiration on the ask if set - if let Some(expiration_info_update) = expiration_info { - // If a removal reward was paid, we need to refund it - if let Some(old_expiration_info) = offer.order_info.expiration_info { - if !old_expiration_info.removal_reward.amount.is_zero() { - response = transfer_coin( - old_expiration_info.removal_reward, - &offer.order_info.creator, - response, - ) - } - } + funds = funds + .sub(details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; - match expiration_info_update { - UpdateVal::Set(new_expiration_info) => { - validate_expiration_info(&env.block, &sudo_params, &new_expiration_info)?; - funds_due.add_assign(new_expiration_info.removal_reward.clone()); - offer.order_info.expiration_info = Some(new_expiration_info); - } - UpdateVal::Unset => { - offer.order_info.expiration_info = None; - } - } - } + // Refund the previous price + funds = funds.add(offer.details.price.clone()); + offer.details = details; offer.save(deps.storage)?; - response = reconcile_funds(&info, funds_due, response)?; - - Ok(response) -} - -/// Creator can accept an offer, whether the token is listed for sale or not. -pub fn execute_accept_offer( - deps: DepsMut, - env: Env, - info: MessageInfo, - collection: Addr, - token_id: TokenId, - creator: Addr, - order_options_str: OrderOptions, -) -> Result { - nonpayable(&info)?; - only_tradable(&deps.querier, &env.block, &collection)?; - only_owner_or_seller(deps.as_ref(), &env, &info, &collection, &token_id)?; - - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; - - let mut response = Response::new(); - - let offer_key = Offer::build_key(&collection, &token_id, &creator); - let offer = offers() - .load(deps.storage, offer_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; + let mut response = transfer_coins(funds.into_vec(), &info.sender, Response::new()); - let ask = Ask::new( - collection.clone(), - token_id.clone(), - offer.order_info.price.clone(), - info.sender, - order_options.asset_recipient, - order_options.finders_fee_bps, - None, + response = response.add_event( + OfferEvent { + ty: "update-offer", + offer: &offer, + attr_keys: vec!["id", "price", "asset_recipient", "finder"], + } + .into(), ); - response = finalize_sale( - deps.as_ref(), - &env, - &ask, - &MatchingOffer::Offer(offer.clone()), - &sudo_params, - order_options.finder.as_ref(), - false, - response, - )?; - - response = offer.remove(deps.storage, false, RewardPayout::Return, response)?; - - // Remove the ask if it exists, refund removal fee if necessary - let ask_option = asks().may_load(deps.storage, ask.key())?; - if let Some(ask) = ask_option { - response = ask.remove(deps.storage, false, RewardPayout::Return, response)?; - } - Ok(response) } -/// Removes an offer made by the creator. Creators can only remove their own offers pub fn execute_remove_offer( deps: DepsMut, _env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, + id: OrderId, ) -> Result { nonpayable(&info)?; - let offer_key = Offer::build_key(&collection, &token_id, &info.sender); let offer = offers() - .load(deps.storage, offer_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("offer not found [{}]", id)))?; - let response = offer.remove(deps.storage, true, RewardPayout::Return, Response::new())?; + ensure_eq!( + info.sender, + offer.creator, + MarketplaceStdError::Unauthorized( + "only the creator of offer can perform this action".to_string() + ) + ); - Ok(response) -} + let refund = offer.details.price.clone(); -/// Removes an offer made by the creator. Only NFT owners can reject an offer made by another creator. -pub fn execute_reject_offer( - deps: DepsMut, - env: Env, - info: MessageInfo, - collection: Addr, - token_id: TokenId, - creator: Addr, -) -> Result { - nonpayable(&info)?; - only_owner_or_seller(deps.as_ref(), &env, &info, &collection, &token_id)?; + offer.remove(deps.storage)?; - let offer_key = Offer::build_key(&collection, &token_id, &creator); - let offer = offers() - .load(deps.storage, offer_key.clone()) - .map_err(|_| ContractError::EntityNotFound(format!("offer {}", offer_key.to_string())))?; + let mut response = transfer_coin(refund, &info.sender, Response::new()); - let response = offer.remove(deps.storage, true, RewardPayout::Return, Response::new())?; + response = response.add_event(Event::new("remove-offer".to_string()).add_attribute("id", id)); Ok(response) } -pub fn execute_remove_expired_offer( +pub fn execute_accept_offer( deps: DepsMut, env: Env, info: MessageInfo, - collection: Addr, - token_id: TokenId, - creator: Addr, + id: OrderId, + asset_recipient: Option, + finder: Option, ) -> Result { - nonpayable(&info)?; - - let offer_key = Offer::build_key(&collection, &token_id, &creator); - let offer = offers() - .load(deps.storage, offer_key.clone()) - .map_err(|_| ContractError::EntityNotFound(format!("offer {}", offer_key.to_string())))?; - - ensure!( - offer.order_info.is_expired(&env.block), - ContractError::EntityNotExpired(format!("offer {}", offer_key.to_string())) - ); + let offer: Offer = offers() + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("offer not found [{}]", id)))?; + + let ask_id = generate_id(vec![offer.collection.as_bytes(), offer.token_id.as_bytes()]); + let ask_option = asks().may_load(deps.storage, ask_id.clone())?; + + // Check if the sender is the owner of the NFT, or if the creator of a valid ask + let ask = if let Some(ask) = ask_option { + if info.sender != ask.creator { + return Err(MarketplaceStdError::Unauthorized( + "sender is not creator of ask".to_string(), + ))?; + } + ask + } else { + let owner_of_response = owner_of(&deps.querier, &offer.collection, &offer.token_id)?; + if info.sender != owner_of_response.owner { + return Err(MarketplaceStdError::Unauthorized( + "sender is not NFT owner".to_string(), + ))?; + } + Ask::new( + info.sender.clone(), + offer.collection.clone(), + offer.token_id.clone(), + OrderDetails { + price: offer.details.price.clone(), + asset_recipient: asset_recipient.clone(), + finder: finder.clone(), + }, + ) + }; - let response = offer.remove( - deps.storage, - true, - RewardPayout::Other(info.sender), + let config: Config = CONFIG.load(deps.storage)?; + let response = finalize_sale( + deps, + &env, + &ask, + &config, + &MatchingOffer::Offer(offer), + false, Response::new(), )?; Ok(response) } -/// Place an offer across an entire collection pub fn execute_set_collection_offer( deps: DepsMut, env: Env, info: MessageInfo, collection: Addr, - price: Coin, - order_options_str: OrderOptions, + details: OrderDetails, ) -> Result { only_tradable(&deps.querier, &env.block, &collection)?; - validate_price(deps.storage, &price)?; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + let mut funds = NativeBalance(info.funds.clone()); + funds.normalize(); + + let nonce = NONCE.load(deps.storage)?.wrapping_add(1); + NONCE.save(deps.storage, &nonce)?; - let mut collection_offer = CollectionOffer::new( - collection.clone(), - price, + let collection_offer = CollectionOffer::new( info.sender.clone(), - order_options.asset_recipient, - order_options.finders_fee_bps, - None, + collection, + details, + env.block.height, + nonce, ); - let collection_offer_key = collection_offer.key(); - // Ensure the collection offer does not exist - ensure!( - collection_offers() - .may_load(deps.storage, collection_offer_key.clone())? - .is_none(), - ContractError::EntityExists(format!( - "collection_offer {}", - collection_offer_key.to_string() - )) - ); + let matching_ask = collection_offer.match_with_ask(deps.as_ref())?; + + let mut response = Response::new(); - let matching_ask = collection_offer.match_with_ask(deps.as_ref(), &env)?; if let Some(ask) = matching_ask { - // If a matching ask is found: - // * ensure at least ask price is paid - // * perform the sale - // * remove the ask - funds_due.add_assign(ask.order_info.price.clone()); + // If a matching ask is found perform the sale + funds = funds + .sub(ask.details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; + let config: Config = CONFIG.load(deps.storage)?; response = finalize_sale( - deps.as_ref(), + deps, &env, &ask, + &config, &MatchingOffer::CollectionOffer(collection_offer), - &sudo_params, - order_options.finder.as_ref(), true, response, )?; - - response = ask.remove(deps.storage, false, RewardPayout::Return, response)?; } else { - // If no match is found, collection offer creation should: - // * ensure offer price is paid - // * store the collection offer - // * take removal fee if applicable - funds_due.add_assign(collection_offer.order_info.price.clone()); - - if let Some(expiration_info) = order_options.expiration_info { - funds_due.add_assign(expiration_info.removal_reward.clone()); - collection_offer.order_info.expiration_info = Some(expiration_info); - } + // If no match is found. Offer creation should store the offer + funds = funds + .sub(collection_offer.details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; + + collection_offer.save(deps.storage)?; - collection_offers().save(deps.storage, collection_offer_key, &collection_offer)?; + response = response.add_event( + CollectionOfferEvent { + ty: "set-collection-offer", + collection_offer: &collection_offer, + attr_keys: vec![ + "id", + "creator", + "collection", + "price", + "asset_recipient", + "finder", + ], + } + .into(), + ) } - response = reconcile_funds(&info, funds_due, response)?; + // Transfer remaining funds back to user + if !funds.is_empty() { + response = transfer_coins(funds.into_vec(), &info.sender, response); + } Ok(response) } -/// A buyer may update their offer on a collection pub fn execute_update_collection_offer( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, - collection: Addr, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + id: OrderId, + details: OrderDetails, ) -> Result { - let api = deps.api; - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - - let mut response = Response::new(); - let mut funds_due = NativeBalance(vec![]); + let allow_denoms = ALLOW_DENOMS.load(deps.storage)?; + ensure!( + allow_denoms.contains(&details.price.denom), + ContractError::InvalidInput("invalid denom".to_string()) + ); - let collection_offer_key = CollectionOffer::build_key(&collection, &info.sender); let mut collection_offer = collection_offers() - .load(deps.storage, collection_offer_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; - - // Update asset recipient if set - match asset_recipient { - Some(UpdateVal::Set(asset_recipient_val)) => { - collection_offer.order_info.asset_recipient = - Some(api.addr_validate(&asset_recipient_val)?); - } - Some(UpdateVal::Unset) => { - collection_offer.order_info.asset_recipient = None; - } - None => {} - } - - // Update finders fee percent if set - match finders_fee_bps { - Some(UpdateVal::Set(finders_fee_bps_val)) => { - collection_offer.order_info.finders_fee_bps = Some(finders_fee_bps_val); - } - Some(UpdateVal::Unset) => { - collection_offer.order_info.finders_fee_bps = None; - } - None => {} - } + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("collection offer not found [{}]", id)))?; - // Update the expiration on the ask if set - if let Some(expiration_info_update) = expiration_info { - // If a removal reward was paid, we need to refund it - if let Some(old_expiration_info) = collection_offer.order_info.expiration_info { - if !old_expiration_info.removal_reward.amount.is_zero() { - response = transfer_coin( - old_expiration_info.removal_reward, - &collection_offer.order_info.creator, - response, - ) - } - } + ensure_eq!( + info.sender, + collection_offer.creator, + MarketplaceStdError::Unauthorized( + "only the creator of collection offer can perform this action".to_string() + ) + ); - match expiration_info_update { - UpdateVal::Set(new_expiration_info) => { - validate_expiration_info(&env.block, &sudo_params, &new_expiration_info)?; - funds_due.add_assign(new_expiration_info.removal_reward.clone()); - collection_offer.order_info.expiration_info = Some(new_expiration_info); - } - UpdateVal::Unset => { - collection_offer.order_info.expiration_info = None; - } - } - } + let mut funds = NativeBalance(info.funds.clone()); + funds.normalize(); - collection_offer.save(deps.storage)?; + funds = funds + .sub(details.price.clone()) + .map_err(|_| ContractError::InsufficientFunds)?; - response = reconcile_funds(&info, funds_due, response)?; + // Refund the previous price + funds = funds.add(collection_offer.details.price.clone()); - Ok(response) -} - -/// Owner of an item in a collection can accept a collection offer -pub fn execute_accept_collection_offer( - deps: DepsMut, - env: Env, - info: MessageInfo, - collection: Addr, - token_id: TokenId, - creator: Addr, - order_options_str: OrderOptions, -) -> Result { - nonpayable(&info)?; - only_tradable(&deps.querier, &env.block, &collection)?; - only_owner_or_seller(deps.as_ref(), &env, &info, &collection, &token_id)?; - - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - let order_options = - validate_order_options(deps.api, &info, &env.block, &sudo_params, order_options_str)?; + collection_offer.details = details; + collection_offer.save(deps.storage)?; - let mut response = Response::new(); + let mut response = transfer_coins(funds.into_vec(), &info.sender, Response::new()); - let collection_offer_key = CollectionOffer::build_key(&collection, &creator); - let collection_offer = collection_offers() - .load(deps.storage, collection_offer_key.clone()) - .map_err(|_| { - ContractError::EntityNotFound(format!( - "collection_offer {}", - collection_offer_key.to_string() - )) - })?; - - let ask = Ask::new( - collection.clone(), - token_id.clone(), - collection_offer.order_info.price.clone(), - info.sender, - order_options.asset_recipient, - order_options.finders_fee_bps, - None, + response = response.add_event( + CollectionOfferEvent { + ty: "update-collection-offer", + collection_offer: &collection_offer, + attr_keys: vec!["id", "price", "asset_recipient", "finder"], + } + .into(), ); - response = finalize_sale( - deps.as_ref(), - &env, - &ask, - &MatchingOffer::CollectionOffer(collection_offer.clone()), - &sudo_params, - order_options.finder.as_ref(), - false, - response, - )?; - - response = collection_offer.remove(deps.storage, false, RewardPayout::Return, response)?; - - // Remove the ask if it exists, refund removal fee if necessary - let ask_key = Ask::build_key(&collection, &token_id); - let ask_option = asks().may_load(deps.storage, ask_key)?; - if let Some(ask) = ask_option { - response = ask.remove(deps.storage, false, RewardPayout::Return, response)?; - } - Ok(response) } -/// Removes an offer made by the creator. Creators can only remove their own offers pub fn execute_remove_collection_offer( deps: DepsMut, _env: Env, info: MessageInfo, - collection: Addr, + id: OrderId, ) -> Result { nonpayable(&info)?; - let collection_offer_key = CollectionOffer::build_key(&collection, &info.sender); let collection_offer = collection_offers() - .load(deps.storage, collection_offer_key.clone()) - .map_err(|_| { - ContractError::EntityNotFound(format!( - "collection_offer {}", - collection_offer_key.to_string() - )) - })?; + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("collection offer not found [{}]", id)))?; - let mut response = Response::new(); + ensure_eq!( + info.sender, + collection_offer.creator, + MarketplaceStdError::Unauthorized( + "only the creator of collection offer can perform this action".to_string() + ) + ); + + let refund = collection_offer.details.price.clone(); - response = collection_offer.remove(deps.storage, true, RewardPayout::Return, response)?; + collection_offer.remove(deps.storage)?; + + let mut response = transfer_coin(refund, &info.sender, Response::new()); + + response = response + .add_event(Event::new("remove-collection-offer".to_string()).add_attribute("id", id)); Ok(response) } -/// Remove an existing collection offer -pub fn execute_remove_expired_collection_offer( +pub fn execute_accept_collection_offer( deps: DepsMut, env: Env, info: MessageInfo, - collection: Addr, - creator: Addr, + id: OrderId, + token_id: TokenId, + asset_recipient: Option, + finder: Option, ) -> Result { - nonpayable(&info)?; - - let collection_offer_key = CollectionOffer::build_key(&collection, &creator); let collection_offer = collection_offers() - .load(deps.storage, collection_offer_key.clone()) - .map_err(|e| ContractError::EntityNotFound(e.to_string()))?; - - ensure!( - collection_offer.order_info.is_expired(&env.block), - ContractError::EntityNotExpired(format!( - "collection_offer {}", - collection_offer_key.to_string() - )) - ); + .load(deps.storage, id.clone()) + .map_err(|_| ContractError::InvalidInput(format!("collection offer not found [{}]", id)))?; + + let ask_id = generate_id(vec![ + collection_offer.collection.as_bytes(), + token_id.as_bytes(), + ]); + let ask_option = asks().may_load(deps.storage, ask_id.clone())?; + + // Check if the sender is the owner of the NFT, or if the creator of a valid ask + let ask = if let Some(ask) = ask_option { + if info.sender != ask.creator { + return Err(MarketplaceStdError::Unauthorized( + "sender is not creator of ask".to_string(), + ))?; + } + ask + } else { + let owner_of_response = owner_of(&deps.querier, &collection_offer.collection, &token_id)?; + if info.sender != owner_of_response.owner { + return Err(MarketplaceStdError::Unauthorized( + "sender is not NFT owner".to_string(), + ))?; + } + Ask::new( + info.sender.clone(), + collection_offer.collection.clone(), + token_id.clone(), + OrderDetails { + price: collection_offer.details.price.clone(), + asset_recipient: asset_recipient.clone(), + finder: finder.clone(), + }, + ) + }; - let response = collection_offer.remove( - deps.storage, - true, - RewardPayout::Other(info.sender), + let config: Config = CONFIG.load(deps.storage)?; + let response = finalize_sale( + deps, + &env, + &ask, + &config, + &MatchingOffer::CollectionOffer(collection_offer), + false, Response::new(), )?; diff --git a/contracts/marketplace-v2/src/helpers.rs b/contracts/marketplace-v2/src/helpers.rs index 0eb361bc..74a38412 100644 --- a/contracts/marketplace-v2/src/helpers.rs +++ b/contracts/marketplace-v2/src/helpers.rs @@ -1,224 +1,231 @@ use crate::{ - constants::MAX_BASIS_POINTS, - msg::OrderOptions, - orders::MatchingOffer, - state::{asks, Ask, Config, ExpirationInfo, OrderInfo, TokenId, PRICE_RANGES}, + orders::{Ask, MatchingOffer}, + state::{Config, TokenId}, ContractError, }; use cosmwasm_std::{ - ensure, ensure_eq, has_coins, Addr, Api, BlockInfo, Coin, Decimal, Deps, Env, Event, - MessageInfo, Storage, + ensure_eq, to_json_binary, Addr, Decimal, Deps, DepsMut, Env, Event, MessageInfo, + QuerierWrapper, Response, WasmMsg, }; -use cw_utils::{maybe_addr, NativeBalance}; -use sg_marketplace_common::{ - coin::transfer_coins, - nft::{owner_of, transfer_nft}, - sale::{FeeType, NftSaleProcessor}, - MarketplaceStdError, +use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; +use sg_marketplace_common::{nft::transfer_nft, sale::NftSaleProcessor, MarketplaceStdError}; +use sha2::{Digest, Sha256}; +use stargaze_royalty_registry::{ + msg::{ExecuteMsg, QueryMsg, RoyaltyPaymentResponse}, + state::RoyaltyEntry, }; -use sg_std::Response; -use stargaze_royalty_registry::fetch_or_set_royalties; use std::{cmp::min, ops::Sub}; -#[allow(clippy::field_reassign_with_default)] -pub fn validate_order_options( - api: &dyn Api, - info: &MessageInfo, - block_info: &BlockInfo, - config: &Config, - order_options_str: OrderOptions, -) -> Result, ContractError> { - let order_options_addr = OrderOptions { - asset_recipient: maybe_addr(api, order_options_str.asset_recipient)?, - finder: maybe_addr(api, order_options_str.finder)?, - finders_fee_bps: order_options_str.finders_fee_bps, - expiration_info: order_options_str.expiration_info, - }; - - if let Some(finder) = &order_options_addr.finder { - ensure!( - finder != info.sender, - ContractError::InvalidInput("finder should not be sender".to_string()) - ); - } - - if let Some(finders_fee_bps) = order_options_str.finders_fee_bps { - ensure!( - finders_fee_bps <= MAX_BASIS_POINTS, - ContractError::InvalidInput("finders_fee_bps is above 100%".to_string()) - ); - } +pub fn build_collection_token_index_str(collection: &str, token_id: &TokenId) -> String { + let string_list = vec![collection.to_string(), token_id.clone()]; + string_list.join("/") +} - if let Some(expiration_info) = &order_options_addr.expiration_info { - validate_expiration_info(block_info, config, expiration_info)?; +pub fn generate_id(components: Vec<&[u8]>) -> String { + let mut hasher = Sha256::new(); + for component in components { + hasher.update(component); } - - Ok(order_options_addr) + format!("{:x}", hasher.finalize()) } -pub fn validate_expiration_info( - block_info: &BlockInfo, - config: &Config, - expiration_info: &ExpirationInfo, +pub fn only_contract_admin( + querier: &QuerierWrapper, + env: &Env, + info: &MessageInfo, ) -> Result<(), ContractError> { - ensure!( - expiration_info.expiration >= block_info.time.plus_seconds(config.min_expiration_seconds), - ContractError::InvalidInput("expiration is below minimum".to_string()) - ); + let contract_info_resp = querier.query_wasm_contract_info(&env.contract.address)?; - ensure!( - has_coins( - &[expiration_info.removal_reward.clone()], - &config.min_removal_reward - ), - ContractError::InvalidInput(format!( - "removal reward must be at least {}", - &config.min_removal_reward - )) + if contract_info_resp.admin == None { + return Err(MarketplaceStdError::Unauthorized( + "contract admin unset".to_string(), + ))?; + } + + ensure_eq!( + info.sender, + contract_info_resp.admin.unwrap(), + MarketplaceStdError::Unauthorized( + "only the admin of contract can perform this action".to_string(), + ) ); Ok(()) } -pub fn validate_price(store: &dyn Storage, price: &Coin) -> Result<(), ContractError> { - let price_range = PRICE_RANGES.may_load(store, price.denom.clone())?; - - ensure!( - price_range.is_some(), - ContractError::InvalidInput("invalid denom".to_string()) - ); - - let price_range = price_range.unwrap(); - ensure!( - price.amount >= price_range.min, - ContractError::InvalidInput(format!( - "price too low {} < {}", - price.amount, price_range.min - )) - ); - ensure!( - price.amount <= price_range.max, - ContractError::InvalidInput(format!( - "price too high {} > {}", - price.amount, price_range.max - )) - ); - Ok(()) +#[derive(Debug)] +pub struct ProtocolFees { + pub protocol_fee: Decimal, + pub maker_reward: Decimal, + pub taker_reward: Decimal, } -/// `reconcile_funds` reconciles the funds due to the contract with the funds sent by the user. -/// If the user sent more funds than due, the excess is returned to the user. -pub fn reconcile_funds( - info: &MessageInfo, - mut funds_due: NativeBalance, - mut response: Response, -) -> Result { - funds_due.normalize(); +pub fn divide_protocol_fees( + config: &Config, + maker_exists: bool, + taker_exists: bool, +) -> Result { + let mut protocol_fees = ProtocolFees { + protocol_fee: Decimal::bps(config.protocol_fee_bps), + maker_reward: Decimal::zero(), + taker_reward: Decimal::zero(), + }; - let mut funds_user = NativeBalance(info.funds.clone()); - funds_user.normalize(); + if protocol_fees.protocol_fee == Decimal::zero() { + return Ok(protocol_fees); + } - // Deduct funds due from user funds - for funds in funds_due.into_vec() { - funds_user = funds_user - .sub(funds.clone()) - .map_err(|_| ContractError::InsufficientFunds { expected: funds })?; + if maker_exists && config.maker_reward_bps > 0 { + protocol_fees.maker_reward = Decimal::bps(config.protocol_fee_bps) + .checked_mul(Decimal::bps(config.maker_reward_bps))?; + protocol_fees.protocol_fee = protocol_fees.protocol_fee.sub(protocol_fees.maker_reward); } - // Transfer remaining funds back to user - if !funds_user.is_empty() { - response = transfer_coins(funds_user.into_vec(), &info.sender, response); + if taker_exists && config.taker_reward_bps > 0 { + protocol_fees.taker_reward = Decimal::bps(config.protocol_fee_bps) + .checked_mul(Decimal::bps(config.taker_reward_bps))?; + protocol_fees.protocol_fee = protocol_fees.protocol_fee.sub(protocol_fees.taker_reward); } - Ok(response) + return Ok(protocol_fees); } -pub fn only_order_creator(info: &MessageInfo, order_info: &OrderInfo) -> Result<(), ContractError> { - ensure_eq!( - info.sender, - order_info.creator, - MarketplaceStdError::Unauthorized( - "only the creator of order can perform this action".to_string() - ) - ); - Ok(()) +pub fn fetch_royalty_entry( + querier: &QuerierWrapper, + royalty_registry: &Addr, + collection: &Addr, + protocol: Option<&Addr>, +) -> Result, ContractError> { + let royalty_payment_response = querier.query_wasm_smart::( + royalty_registry, + &QueryMsg::RoyaltyPayment { + collection: collection.to_string(), + protocol: protocol.map(|p| p.to_string()), + }, + )?; + + if let Some(royalty_protocol) = royalty_payment_response.royalty_protocol { + return Ok(Some(royalty_protocol.royalty_entry)); + } + + if let Some(royalty_default) = royalty_payment_response.royalty_default { + return Ok(Some(royalty_default.royalty_entry)); + } + + Ok(None) } -pub fn only_owner_or_seller( +pub fn fetch_or_set_royalties( deps: Deps, - env: &Env, - info: &MessageInfo, + royalty_registry: &Addr, collection: &Addr, - token_id: &TokenId, -) -> Result<(), MarketplaceStdError> { - let owner_of_response = owner_of(&deps.querier, collection, token_id)?; - - if owner_of_response.owner == info.sender { - return Ok(()); + protocol: Option<&Addr>, + mut response: Response, +) -> Result<(Option, Response), ContractError> { + let royalty_entry = fetch_royalty_entry(&deps.querier, royalty_registry, collection, protocol)?; + if let Some(royalty_entry) = royalty_entry { + return Ok((Some(royalty_entry), response)); } - if owner_of_response.owner == env.contract.address { - let ask_key = Ask::build_key(collection, token_id); - let ask_option = asks().may_load(deps.storage, ask_key)?; - - if let Some(ask) = ask_option { - if ask.order_info.creator == info.sender { - return Ok(()); - } - } + let collection_info: CollectionInfoResponse = deps + .querier + .query_wasm_smart(collection, &Sg721QueryMsg::CollectionInfo {})?; + + if let Some(royalty_info_response) = collection_info.royalty_info { + let royalty_entry = RoyaltyEntry { + recipient: deps + .api + .addr_validate(&royalty_info_response.payment_address)?, + share: royalty_info_response.share, + updated: None, + }; + + response = response.add_message(WasmMsg::Execute { + contract_addr: royalty_registry.to_string(), + msg: to_json_binary(&ExecuteMsg::InitializeCollectionRoyalty { + collection: collection.to_string(), + }) + .unwrap(), + funds: vec![], + }); + + return Ok((Some(royalty_entry), response)); } - Err(MarketplaceStdError::Unauthorized( - "sender is not owner or seller".to_string(), - )) + Ok((None, response)) } -/// Processes a sale, transferring funds to correct destinations pub fn finalize_sale( - deps: Deps, + deps: DepsMut, env: &Env, ask: &Ask, - matching_offer: &MatchingOffer, config: &Config, - finder: Option<&Addr>, + matching_offer: &MatchingOffer, ask_before_offer: bool, response: Response, ) -> Result { - let (offer_price, offer_order_info) = match &matching_offer { - MatchingOffer::Offer(offer) => (offer.order_info.price.clone(), offer.order_info.clone()), + let (nft_recipient, offer_details) = match &matching_offer { + MatchingOffer::Offer(offer) => (offer.asset_recipient(), &offer.details), MatchingOffer::CollectionOffer(collection_offer) => ( - collection_offer.order_info.price.clone(), - collection_offer.order_info.clone(), + collection_offer.asset_recipient(), + &collection_offer.details, ), }; - let (sale_price, finders_fee_bps) = if ask_before_offer { - (ask.order_info.price.clone(), ask.order_info.finders_fee_bps) + let (sale_price, maker, taker) = if ask_before_offer { + ( + &ask.details.price, + &ask.details.finder, + &offer_details.finder, + ) } else { - (offer_price, offer_order_info.finders_fee_bps) + ( + &offer_details.price, + &offer_details.finder, + &ask.details.finder, + ) }; + let seller_recipient = ask.asset_recipient(); + let mut nft_sale_processor = + NftSaleProcessor::new(sale_price.clone(), seller_recipient.clone()); + + let protocol_fees = divide_protocol_fees(config, maker.is_some(), taker.is_some())?; + + if protocol_fees.protocol_fee > Decimal::zero() { + nft_sale_processor.add_fee( + "protocol".to_string(), + protocol_fees.protocol_fee, + config.fee_manager.clone(), + ); + } + if protocol_fees.maker_reward > Decimal::zero() { + nft_sale_processor.add_fee( + "maker".to_string(), + protocol_fees.maker_reward, + maker.clone().unwrap().clone(), + ); + } + if protocol_fees.taker_reward > Decimal::zero() { + nft_sale_processor.add_fee( + "taker".to_string(), + protocol_fees.taker_reward, + taker.clone().unwrap().clone(), + ); + } + let (royalty_entry_option, mut response) = fetch_or_set_royalties( - deps, + deps.as_ref(), &config.royalty_registry, &ask.collection, Some(&env.contract.address), response, )?; - let mut nft_sale_processor = - NftSaleProcessor::new(sale_price.clone(), ask.order_info.asset_recipient()); - - nft_sale_processor.add_fee( - FeeType::FairBurn, - Decimal::bps(config.trading_fee_bps), - config.fair_burn.clone(), - ); - if let Some(royalty_entry) = royalty_entry_option { nft_sale_processor.add_fee( - FeeType::Royalty, + "royalty".to_string(), min( royalty_entry.share, Decimal::bps(config.max_royalty_fee_bps), @@ -227,41 +234,71 @@ pub fn finalize_sale( ); } - if let (Some(finder), Some(finders_fee_bps)) = (finder, finders_fee_bps) { - nft_sale_processor.add_fee( - FeeType::Finder, - min( - Decimal::bps(finders_fee_bps), - Decimal::bps(config.max_finders_fee_bps), - ), - finder.clone(), - ); - } - nft_sale_processor.build_payments()?; response = nft_sale_processor.payout(response); // Transfer NFT to buyer - response = transfer_nft( - &ask.collection, - &ask.token_id, - &offer_order_info.asset_recipient(), - response, - ); + response = transfer_nft(&ask.collection, &ask.token_id, &nft_recipient, response); - response = response.add_event( - Event::new("finalize-sale") - .add_attribute("collection", ask.collection.to_string()) - .add_attribute("token_id", ask.token_id.to_string()) - .add_attribute("seller", ask.order_info.creator.to_string()) - .add_attribute("buyer", offer_order_info.creator.to_string()) - .add_attribute("price", sale_price.to_string()), - ); + // Remove orders + ask.remove(deps.storage)?; + match &matching_offer { + MatchingOffer::Offer(offer) => { + offer.remove(deps.storage)?; + } + MatchingOffer::CollectionOffer(collection_offer) => { + collection_offer.remove(deps.storage)?; + } + } + + let mut sale_event = Event::new("finalize-sale") + .add_attribute("collection", ask.collection.to_string()) + .add_attribute("token_id", ask.token_id.to_string()) + .add_attribute("sale_price", sale_price.to_string()) + .add_attribute("seller_recipient", seller_recipient.to_string()) + .add_attribute("nft_recipient", nft_recipient.to_string()) + .add_attribute("ask", ask.id.to_string()); + + match &matching_offer { + MatchingOffer::Offer(offer) => { + sale_event = sale_event.add_attribute("offer", offer.id.to_string()); + } + MatchingOffer::CollectionOffer(collection_offer) => { + sale_event = + sale_event.add_attribute("collection_offer", collection_offer.id.to_string()); + } + } + + for payment in nft_sale_processor.payments.iter() { + sale_event = sale_event.add_attribute(&payment.label, payment.funds.to_string()); + } + + response = response.add_event(sale_event); Ok(response) } -pub fn build_collection_token_index_str(collection: &str, token_id: &TokenId) -> String { - let string_list = vec![collection.to_string(), token_id.clone()]; - string_list.join("/") +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn try_maker_and_taker_fees() { + let config = Config { + fee_manager: Addr::unchecked("fee_manager"), + royalty_registry: Addr::unchecked("royalty_registry"), + protocol_fee_bps: 200, + max_royalty_fee_bps: 500, + maker_reward_bps: 4000, + taker_reward_bps: 1000, + }; + + let result = divide_protocol_fees(&config, true, true).unwrap(); + + assert_eq!(result.protocol_fee, Decimal::from_str("0.01").unwrap()); + assert_eq!(result.maker_reward, Decimal::from_str("0.008").unwrap()); + assert_eq!(result.taker_reward, Decimal::from_str("0.002").unwrap()); + } } diff --git a/contracts/marketplace-v2/src/instantiate.rs b/contracts/marketplace-v2/src/instantiate.rs index c60e8d60..d357f29a 100644 --- a/contracts/marketplace-v2/src/instantiate.rs +++ b/contracts/marketplace-v2/src/instantiate.rs @@ -1,56 +1,59 @@ -use cosmwasm_std::{ensure, DepsMut, Env, MessageInfo}; -use cw2::set_contract_version; -use sg_std::Response; - use crate::{ constants::{CONTRACT_NAME, CONTRACT_VERSION}, error::ContractError, + events::{AllowDenomsEvent, ConfigEvent}, msg::InstantiateMsg, - state::{Config, PriceRange, PRICE_RANGES}, + state::{ALLOW_DENOMS, NONCE}, }; +use cosmwasm_std::{DepsMut, Env, Event, MessageInfo, Response}; +use cw2::set_contract_version; + #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let config = Config { - fair_burn: deps.api.addr_validate(&msg.config.fair_burn)?, - royalty_registry: deps.api.addr_validate(&msg.config.royalty_registry)?, - listing_fee: msg.config.listing_fee, - min_removal_reward: msg.config.min_removal_reward, - trading_fee_bps: msg.config.trading_fee_bps, - max_royalty_fee_bps: msg.config.max_royalty_fee_bps, - max_finders_fee_bps: msg.config.max_finders_fee_bps, - min_expiration_seconds: msg.config.min_expiration_seconds, - order_removal_lookahead_secs: msg.config.order_removal_lookahead_secs, - max_asks_removed_per_block: msg.config.max_asks_removed_per_block, - max_offers_removed_per_block: msg.config.max_offers_removed_per_block, - max_collection_offers_removed_per_block: msg.config.max_collection_offers_removed_per_block, - }; - config.validate()?; + // Map the Config Strings to Addrs and then save it + let config = msg.config.str_to_addr(deps.api)?; config.save(deps.storage)?; - for (denom, price_range) in msg.price_ranges { - PRICE_RANGES.update( - deps.storage, - denom, - |existing_price_range| -> Result { - ensure!( - existing_price_range.is_none(), - ContractError::InvalidInput("duplicate denom in price_ranges".to_string()) - ); - Ok(price_range) - }, - )?; - } - - Ok(Response::new()) + ALLOW_DENOMS.save(deps.storage, &msg.allow_denoms)?; + + NONCE.save(deps.storage, &0)?; + + let contract_info_response = deps + .querier + .query_wasm_contract_info(env.contract.address)?; + + let instantiate_event = Event::new("instantiate".to_string()) + .add_attribute("code_id", contract_info_response.code_id.to_string()) + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION); + + let response = Response::new() + .add_event(instantiate_event) + .add_event( + ConfigEvent { + ty: "set-config", + config: &config, + } + .into(), + ) + .add_event( + AllowDenomsEvent { + ty: "set-allow-denoms", + allow_denoms: &msg.allow_denoms, + } + .into(), + ); + + Ok(response) } diff --git a/contracts/marketplace-v2/src/lib.rs b/contracts/marketplace-v2/src/lib.rs index d9cf8bd8..594368eb 100644 --- a/contracts/marketplace-v2/src/lib.rs +++ b/contracts/marketplace-v2/src/lib.rs @@ -1,16 +1,13 @@ mod error; #[allow(clippy::too_many_arguments)] pub mod execute; -#[allow(clippy::too_many_arguments)] -pub mod helpers; pub mod msg; pub mod query; pub mod state; -#[allow(clippy::too_many_arguments)] -pub mod sudo; pub use error::ContractError; pub mod constants; pub mod events; +pub mod helpers; pub mod instantiate; pub mod migrate; pub mod orders; diff --git a/contracts/marketplace-v2/src/migrate.rs b/contracts/marketplace-v2/src/migrate.rs index da774daa..2a840f60 100644 --- a/contracts/marketplace-v2/src/migrate.rs +++ b/contracts/marketplace-v2/src/migrate.rs @@ -1,7 +1,6 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{DepsMut, Env}; +use cosmwasm_std::{DepsMut, Env, Response}; use cw2::set_contract_version; -use sg_std::Response; use crate::{ constants::{CONTRACT_NAME, CONTRACT_VERSION}, diff --git a/contracts/marketplace-v2/src/msg.rs b/contracts/marketplace-v2/src/msg.rs index 14b37cdd..a9d8245f 100644 --- a/contracts/marketplace-v2/src/msg.rs +++ b/contracts/marketplace-v2/src/msg.rs @@ -1,346 +1,143 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Coin}; -use cw_address_like::AddressLike; -use sg_index_query::QueryOptions; - -use crate::state::{ - Ask, CollectionOffer, Config, Denom, ExpirationInfo, Offer, PriceRange, TokenId, +use crate::{ + orders::{Ask, CollectionOffer, Offer, OrderDetails}, + state::{AllowDenoms, Config, Denom, OrderId, TokenId}, }; -#[cw_serde] -pub enum UpdateVal { - Set(T), - Unset, -} - -#[cw_serde] -pub struct OrderOptions { - pub asset_recipient: Option, - pub finder: Option, - pub finders_fee_bps: Option, - pub expiration_info: Option, -} - -impl Default for OrderOptions { - fn default() -> Self { - OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: None, - } - } -} +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Addr; +use sg_index_query::QueryOptions; #[cw_serde] pub struct InstantiateMsg { /// The initial configuration for the contract pub config: Config, - /// Min/max values for offers and asks - pub price_ranges: Vec<(Denom, PriceRange)>, + /// The initial allowed denoms for the contract + pub allow_denoms: AllowDenoms, } #[cw_serde] pub enum ExecuteMsg { - /// List an NFT on the marketplace by creating a new ask - SetAsk { - collection: String, - token_id: TokenId, - price: Coin, - order_options: Option>, + // Admin messages + UpdateConfig { + config: Config, }, - /// Update the price of an existing ask - UpdateAsk { - collection: String, - token_id: TokenId, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + UpdateAllowDenoms { + allow_denoms: AllowDenoms, }, - /// Buy an NFT from the marketplace - AcceptAsk { + // Marketplace messages + SetAsk { collection: String, token_id: TokenId, - order_options: Option>, + details: OrderDetails, }, - /// Remove an existing ask from the marketplace RemoveAsk { - collection: String, - token_id: TokenId, - }, - /// Privileged operation to remove stale or invalid asks. - RemoveExpiredAsk { - collection: String, - token_id: TokenId, + id: OrderId, }, - /// Create an offer for an NFT - SetOffer { - collection: String, - token_id: TokenId, - price: Coin, - order_options: Option>, + UpdateAsk { + id: OrderId, + details: OrderDetails, }, - /// Update the price of an existing offer - UpdateOffer { - collection: String, - token_id: TokenId, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + AcceptAsk { + id: OrderId, + asset_recipient: Option, + finder: Option, }, - /// Accept a offer on an existing ask - AcceptOffer { + SetOffer { collection: String, token_id: TokenId, - creator: String, - order_options: Option>, + details: OrderDetails, }, - /// Remove an existing offer from an ask RemoveOffer { - collection: String, - token_id: TokenId, + id: OrderId, }, - /// Reject a offer on an existing ask - RejectOffer { - collection: String, - token_id: TokenId, - creator: String, + UpdateOffer { + id: OrderId, + details: OrderDetails, }, - /// Remove an existing offer from an ask - RemoveExpiredOffer { - collection: String, - token_id: TokenId, - creator: String, + AcceptOffer { + id: OrderId, + asset_recipient: Option, + finder: Option, }, - /// Place an offer (limit order) across an entire collection SetCollectionOffer { collection: String, - price: Coin, - order_options: Option>, + details: OrderDetails, + }, + RemoveCollectionOffer { + id: OrderId, }, - /// Update the price of an existing offer UpdateCollectionOffer { - collection: String, - asset_recipient: Option>, - finders_fee_bps: Option>, - expiration_info: Option>, + id: OrderId, + details: OrderDetails, }, - /// Accept a collection offer AcceptCollectionOffer { - collection: String, + id: OrderId, token_id: TokenId, - creator: String, - order_options: Option>, - }, - /// Remove an offer across an entire collection - RemoveCollectionOffer { collection: String }, - /// Remove an offer across an entire collection - RemoveExpiredCollectionOffer { collection: String, creator: String }, -} - -#[allow(clippy::large_enum_variant)] -#[cw_serde] -pub enum SudoMsg { - /// BeginBlock Is called by x/cron module BeginBlocker - BeginBlock {}, - /// EndBlock Is called by x/cron module EndBlocker - EndBlock {}, - /// Update the contract parameters - /// Can only be called by governance - UpdateParams { - fair_burn: Option, - listing_fee: Option, - min_removal_reward: Option, - trading_fee_bps: Option, - max_royalty_fee_bps: Option, - max_finders_fee_bps: Option, - min_expiration_seconds: Option, - max_asks_removed_per_block: Option, - max_offers_removed_per_block: Option, - max_collection_offers_removed_per_block: Option, - }, - AddDenoms { - price_ranges: Vec<(Denom, PriceRange)>, - }, - RemoveDenoms { - denoms: Vec, + asset_recipient: Option, + finder: Option, }, } -#[cw_serde] -pub struct AsksByCollectionOffset { - pub token_id: TokenId, -} - -#[cw_serde] -pub struct AsksByPriceOffset { - pub token_id: TokenId, - pub amount: u128, -} - -#[cw_serde] -pub struct AsksByCreatorOffset { - pub collection: String, - pub token_id: TokenId, -} - -#[cw_serde] -pub struct AsksByExpirationOffset { - pub collection: String, - pub token_id: TokenId, - pub expiration: u64, -} - -#[cw_serde] -pub struct OffersByCollectionOffset { - pub token_id: TokenId, - pub creator: String, -} - -#[cw_serde] -pub struct OffersByTokenPriceOffset { - pub creator: String, - pub amount: u128, -} - -#[cw_serde] -pub struct OffersByCreatorOffset { - pub collection: String, - pub token_id: TokenId, -} - -#[cw_serde] -pub struct OffersByExpirationOffset { - pub collection: String, - pub token_id: TokenId, - pub creator: String, - pub expiration: u64, -} - -#[cw_serde] -pub struct CollectionOffersByCollectionOffset { - pub creator: String, -} - -#[cw_serde] -pub struct CollectionOffersByPriceOffset { - pub creator: String, - pub amount: u128, -} - -#[cw_serde] -pub struct CollectionOffersByCreatorOffset { - pub collection: String, -} - -#[cw_serde] -pub struct CollectionOffersByExpirationOffset { - pub collection: String, - pub creator: String, - pub expiration: u64, -} - #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { - /// Get the config for the contract #[returns(Config)] Config {}, - /// Get the config for the contract - #[returns(PriceRange)] - PriceRange { denom: Denom }, - /// Get the config for the contract - #[returns(Vec<(Denom, PriceRange)>)] - PriceRanges { - query_options: Option>, - }, - /// Get the current ask for specific NFT + #[returns(AllowDenoms)] + AllowDenoms {}, #[returns(Option)] - Ask { - collection: String, - token_id: TokenId, - }, - /// Get all asks for a collection + Ask(String), #[returns(Vec)] - AsksByCollection { - collection: String, - query_options: Option>, - }, - /// Get all asks for a collection, sorted by price + Asks(Vec), #[returns(Vec)] - AsksByPrice { + AsksByCollectionDenom { collection: String, denom: Denom, - query_options: Option>, + query_options: Option>, }, - /// Get all asks by creator #[returns(Vec)] - AsksByCreator { + AsksByCreatorCollection { creator: String, - query_options: Option>, - }, - /// Get all asks sorted by the expiration time - #[returns(Vec)] - AsksByExpiration { - query_options: Option>, - }, - /// Get data for a specific offer - #[returns(Offer)] - Offer { collection: String, - token_id: TokenId, - creator: String, + query_options: Option>, }, - /// Get all offers on a collection + #[returns(Option)] + Offer(String), #[returns(Vec)] - OffersByCollection { - collection: String, - query_options: Option>, - }, - /// Get all offers on a collection token, sorted by price + Offers(Vec), #[returns(Vec)] OffersByTokenPrice { collection: String, token_id: TokenId, denom: Denom, - query_options: Option>, + query_options: Option>, }, - /// Get all offers made by a creator #[returns(Vec)] - OffersByCreator { + OffersByCreatorCollection { creator: String, - query_options: Option>, - }, - /// Get all offers sorted by the expiration time - #[returns(Vec)] - OffersByExpiration { - query_options: Option>, - }, - /// Get data for a specific collection offer - #[returns(Option)] - CollectionOffer { collection: String, creator: String }, - /// Get data for collection offers sorted by collection - #[returns(Vec)] - CollectionOffersByCollection { collection: String, - query_options: Option>, + query_options: Option>, }, - /// Get all collection offers for a collection, sorted by price + #[returns(Option)] + CollectionOffer(String), + #[returns(Vec)] + CollectionOffers(Vec), #[returns(Vec)] CollectionOffersByPrice { collection: String, denom: Denom, - query_options: Option>, + query_options: Option>, }, - /// Get all collection offers made by a creator #[returns(Vec)] - CollectionOffersByCreator { + CollectionOffersByCreatorCollection { creator: String, - query_options: Option>, - }, - /// Get all collection offers sorted by the expiration time - #[returns(Vec)] - CollectionOffersByExpiration { - query_options: Option>, + collection: String, + query_options: Option>, }, } + +#[cw_serde] +pub struct PriceOffset { + pub id: OrderId, + pub amount: u128, +} diff --git a/contracts/marketplace-v2/src/orders.rs b/contracts/marketplace-v2/src/orders.rs index 2b6d82ae..04fc6c07 100644 --- a/contracts/marketplace-v2/src/orders.rs +++ b/contracts/marketplace-v2/src/orders.rs @@ -1,136 +1,97 @@ use crate::{ - msg::{AsksByPriceOffset, CollectionOffersByPriceOffset, OffersByTokenPriceOffset}, - query::{query_asks_by_price, query_collection_offers_by_price, query_offers_by_token_price}, - state::{ - asks, collection_offers, offers, Ask, AskKey, CollectionOffer, CollectionOfferKey, - ExpirationInfo, Offer, OfferKey, OrderInfo, TokenId, + events::AskEvent, + helpers::{finalize_sale, generate_id}, + msg::PriceOffset, + query::{ + query_asks_by_collection_denom, query_collection_offers_by_price, + query_offers_by_token_price, }, + state::{asks, collection_offers, offers, Config, TokenId}, ContractError, }; +use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - attr, has_coins, Addr, Attribute, BlockInfo, Coin, Deps, Env, Storage, Uint128, + attr, has_coins, Addr, Api, Attribute, Coin, Deps, DepsMut, Env, Response, StdResult, Storage, }; -use cw_utils::NativeBalance; +use cw_address_like::AddressLike; +use cw_utils::maybe_addr; use sg_index_query::{QueryBound, QueryOptions}; -use sg_marketplace_common::{ - address::address_or, - coin::{transfer_coin, transfer_coins}, - nft::transfer_nft, -}; -use sg_std::Response; -use std::ops::AddAssign; +use sg_marketplace_common::{address::address_or, nft::transfer_nft}; -pub enum MatchingOffer { - Offer(Offer), - CollectionOffer(CollectionOffer), +#[cw_serde] +pub struct OrderDetails { + pub price: Coin, + pub asset_recipient: Option, + pub finder: Option, } -pub enum RewardPayout { - Contract, - Return, - Other(Addr), +impl OrderDetails { + pub fn str_to_addr(self, api: &dyn Api) -> StdResult> { + Ok(OrderDetails { + price: self.price, + asset_recipient: maybe_addr(api, self.asset_recipient)?, + finder: maybe_addr(api, self.finder)?, + }) + } } -impl OrderInfo { - pub fn asset_recipient(&self) -> Addr { - address_or(self.asset_recipient.as_ref(), &self.creator) - } +pub enum MatchingOffer { + Offer(Offer), + CollectionOffer(CollectionOffer), +} - pub fn is_expired(&self, block_info: &BlockInfo) -> bool { - if let Some(expiration_info) = &self.expiration_info { - expiration_info.expiration <= block_info.time - } else { - false - } - } +#[cw_serde] +pub struct Ask { + pub id: String, + pub creator: Addr, + pub collection: Addr, + pub token_id: TokenId, + pub details: OrderDetails, } impl Ask { pub fn new( + creator: Addr, collection: Addr, token_id: TokenId, - price: Coin, - creator: Addr, - asset_recipient: Option, - finders_fee_bps: Option, - expiration_info: Option, + details: OrderDetails, ) -> Self { - Ask { + Self { + id: generate_id(vec![collection.as_bytes(), token_id.as_bytes()]), + creator, collection, token_id, - order_info: OrderInfo { - price, - creator, - asset_recipient, - finders_fee_bps, - expiration_info, - }, + details, } } - pub fn build_key(collection: &Addr, token_id: &TokenId) -> AskKey { - (collection.to_string(), token_id.clone()) - } - - pub fn key(&self) -> AskKey { - Self::build_key(&self.collection, &self.token_id) + pub fn asset_recipient(&self) -> Addr { + address_or(self.details.asset_recipient.as_ref(), &self.creator) } pub fn save(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { - asks().save(storage, self.key(), self)?; + asks().save(storage, self.id.clone(), self)?; Ok(()) } - pub fn remove( - &self, - storage: &mut dyn Storage, - refund_nft: bool, - reward_payout: RewardPayout, - mut response: Response, - ) -> Result { - asks().remove(storage, self.key())?; - - let return_address = self.order_info.asset_recipient(); - - if refund_nft { - response = transfer_nft(&self.collection, &self.token_id, &return_address, response); - } - - if let Some(expiration_info) = &self.order_info.expiration_info { - if expiration_info.removal_reward.amount > Uint128::zero() { - let reward = expiration_info.removal_reward.clone(); - match &reward_payout { - RewardPayout::Contract => {} - RewardPayout::Return => { - response = transfer_coin(reward, &return_address, response); - } - RewardPayout::Other(recipient) => { - response = transfer_coin(reward, recipient, response); - } - } - } - } - - Ok(response) + pub fn remove(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { + asks().remove(storage, self.id.clone())?; + Ok(()) } - pub fn match_with_offer( - &self, - deps: Deps, - _env: &Env, - ) -> Result, ContractError> { + fn match_with_offer(&self, deps: Deps) -> Result, ContractError> { let top_offer = query_offers_by_token_price( deps, self.collection.clone(), self.token_id.clone(), - self.order_info.price.denom.clone(), + self.details.price.denom.clone(), QueryOptions { descending: Some(true), limit: Some(1), - min: Some(QueryBound::Inclusive(OffersByTokenPriceOffset { - creator: "".to_string(), - amount: self.order_info.price.amount.u128(), + min: Some(QueryBound::Inclusive(PriceOffset { + id: "".to_string(), + amount: self.details.price.amount.u128(), })), max: None, }, @@ -140,13 +101,13 @@ impl Ask { let top_collection_offer = query_collection_offers_by_price( deps, self.collection.clone(), - self.order_info.price.denom.clone(), + self.details.price.denom.clone(), QueryOptions { descending: Some(true), limit: Some(1), - min: Some(QueryBound::Inclusive(CollectionOffersByPriceOffset { - creator: "".to_string(), - amount: self.order_info.price.amount.u128(), + min: Some(QueryBound::Inclusive(PriceOffset { + id: "".to_string(), + amount: self.details.price.amount.u128(), })), max: None, }, @@ -155,7 +116,7 @@ impl Ask { let result = match (top_offer, top_collection_offer) { (Some(offer), Some(collection_offer)) => { - if offer.order_info.price.amount >= collection_offer.order_info.price.amount { + if offer.details.price.amount >= collection_offer.details.price.amount { Some(MatchingOffer::Offer(offer)) } else { Some(MatchingOffer::CollectionOffer(collection_offer)) @@ -171,43 +132,73 @@ impl Ask { Ok(result) } + pub fn push_order( + &self, + deps: DepsMut, + env: &Env, + config: &Config, + mut response: Response, + ) -> Result { + let match_result = self.match_with_offer(deps.as_ref())?; + + if let Some(matching_offer) = match_result { + // If a match is found finalize the sale + finalize_sale(deps, env, &self, config, &matching_offer, false, response) + } else { + // If no match is found continue creating the ask. + // Ask creation should: + // * escrow the nft + // * store the ask + + response = transfer_nft( + &self.collection, + &self.token_id, + &env.contract.address, + response, + ); + + self.save(deps.storage)?; + + response = response.add_event( + AskEvent { + ty: "set-ask", + ask: &self, + attr_keys: vec![ + "id", + "creator", + "collection", + "token_id", + "price", + "asset_recipient", + "finder", + ], + } + .into(), + ); + + Ok(response) + } + } + pub fn get_event_attrs(&self, attr_keys: Vec<&str>) -> Vec { let mut attributes = vec![]; - for attr_key in attr_keys { let attr = match attr_key { + "id" => Some(attr("id", self.id.to_string())), + "creator" => Some(attr("creator", self.creator.to_string())), "collection" => Some(attr("collection", self.collection.to_string())), "token_id" => Some(attr("token_id", self.token_id.to_string())), - "creator" => Some(attr("creator", self.order_info.creator.to_string())), - "price" => Some(attr("price", self.order_info.price.to_string())), + "price" => Some(attr("price", self.details.price.to_string())), "asset_recipient" => self - .order_info + .details .asset_recipient .as_ref() .map(|asset_recipient| attr("asset_recipient", asset_recipient.to_string())), - "finders_fee_bps" => self - .order_info - .finders_fee_bps + "finder" => self + .details + .finder .as_ref() - .map(|finders_fee_bps| attr("finders_fee_bps", finders_fee_bps.to_string())), - "expiration" => self - .order_info - .expiration_info - .as_ref() - .map(|expiration_info| { - attr("expiration_info", expiration_info.expiration.to_string()) - }), - "removal_reward" => { - self.order_info - .expiration_info - .as_ref() - .map(|expiration_info| { - attr( - "expiration_info", - expiration_info.removal_reward.to_string(), - ) - }) - } + .map(|finder| attr("finder", finder.to_string())), &_ => { unreachable!("Invalid attr_key: {}", attr_key) } @@ -216,185 +207,155 @@ impl Ask { attributes.push(value); } } - attributes } } +#[cw_serde] +pub struct Offer { + pub id: String, + pub creator: Addr, + pub collection: Addr, + pub token_id: TokenId, + pub details: OrderDetails, +} + impl Offer { pub fn new( + creator: Addr, collection: Addr, token_id: TokenId, - price: Coin, - creator: Addr, - asset_recipient: Option, - finders_fee_bps: Option, - expiration_info: Option, + details: OrderDetails, + height: u64, + nonce: u64, ) -> Self { Self { + id: generate_id(vec![ + collection.as_bytes(), + token_id.as_bytes(), + height.to_be_bytes().as_ref(), + nonce.to_be_bytes().as_ref(), + ]), + creator, collection, token_id, - order_info: OrderInfo { - price, - creator, - asset_recipient, - finders_fee_bps, - expiration_info, - }, + details, } } - pub fn build_key(collection: &Addr, token_id: &TokenId, creator: &Addr) -> OfferKey { - (collection.clone(), token_id.clone(), creator.clone()) - } - - pub fn key(&self) -> OfferKey { - Self::build_key(&self.collection, &self.token_id, &self.order_info.creator) + pub fn asset_recipient(&self) -> Addr { + address_or(self.details.asset_recipient.as_ref(), &self.creator) } pub fn save(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { - offers().save(storage, self.key(), self)?; + offers().save(storage, self.id.clone(), self)?; Ok(()) } - pub fn remove( - &self, - storage: &mut dyn Storage, - refund_offer: bool, - reward_payout: RewardPayout, - mut response: Response, - ) -> Result { - offers().remove(storage, self.key())?; - - let return_address = self.order_info.asset_recipient(); - let mut funds_to_return = NativeBalance(vec![]); - - if refund_offer { - funds_to_return.add_assign(self.order_info.price.clone()); - } - - if let Some(expiration_info) = &self.order_info.expiration_info { - if expiration_info.removal_reward.amount > Uint128::zero() { - let reward = expiration_info.removal_reward.clone(); - match &reward_payout { - RewardPayout::Contract => {} - RewardPayout::Return => { - funds_to_return.add_assign(reward); - } - RewardPayout::Other(recipient) => { - response = transfer_coin(reward, recipient, response); - } - } - } - } - - if !funds_to_return.is_empty() { - funds_to_return.normalize(); - response = transfer_coins(funds_to_return.into_vec(), &return_address, response); - } - - Ok(response) + pub fn remove(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { + offers().remove(storage, self.id.clone())?; + Ok(()) } - pub fn match_with_ask(&self, deps: Deps, _env: &Env) -> Result, ContractError> { - let ask_key = Ask::build_key(&self.collection, &self.token_id); - let ask_option = asks().may_load(deps.storage, ask_key)?; + pub fn match_with_ask(&self, deps: Deps) -> Result, ContractError> { + let ask_id: String = + generate_id(vec![self.collection.as_bytes(), self.token_id.as_bytes()]); + let ask_option = asks().may_load(deps.storage, ask_id)?; if let Some(ask) = ask_option { - if has_coins(&[self.order_info.price.clone()], &ask.order_info.price) { + if has_coins(&[self.details.price.clone()], &ask.details.price) { return Ok(Some(ask)); } }; Ok(None) } + + pub fn get_event_attrs(&self, attr_keys: Vec<&str>) -> Vec { + let mut attributes = vec![]; + for attr_key in attr_keys { + let attr = match attr_key { + "id" => Some(attr("id", self.id.to_string())), + "creator" => Some(attr("creator", self.creator.to_string())), + "collection" => Some(attr("collection", self.collection.to_string())), + "token_id" => Some(attr("token_id", self.token_id.to_string())), + "price" => Some(attr("price", self.details.price.to_string())), + "asset_recipient" => self + .details + .asset_recipient + .as_ref() + .map(|asset_recipient| attr("asset_recipient", asset_recipient.to_string())), + "finder" => self + .details + .finder + .as_ref() + .map(|finder| attr("finder", finder.to_string())), + &_ => { + unreachable!("Invalid attr_key: {}", attr_key) + } + }; + if let Some(value) = attr { + attributes.push(value); + } + } + attributes + } +} + +#[cw_serde] +pub struct CollectionOffer { + pub id: String, + pub creator: Addr, + pub collection: Addr, + pub details: OrderDetails, } impl CollectionOffer { pub fn new( - collection: Addr, - price: Coin, creator: Addr, - asset_recipient: Option, - finders_fee_bps: Option, - expiration_info: Option, + collection: Addr, + details: OrderDetails, + height: u64, + nonce: u64, ) -> Self { Self { + id: generate_id(vec![ + collection.as_bytes(), + height.to_be_bytes().as_ref(), + nonce.to_be_bytes().as_ref(), + ]), + creator, collection, - order_info: OrderInfo { - price, - creator, - asset_recipient, - finders_fee_bps, - expiration_info, - }, + details, } } - pub fn build_key(collection: &Addr, creator: &Addr) -> CollectionOfferKey { - (collection.clone(), creator.clone()) - } - - pub fn key(&self) -> CollectionOfferKey { - Self::build_key(&self.collection, &self.order_info.creator) + pub fn asset_recipient(&self) -> Addr { + address_or(self.details.asset_recipient.as_ref(), &self.creator) } pub fn save(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { - collection_offers().save(storage, self.key(), self)?; + collection_offers().save(storage, self.id.clone(), self)?; Ok(()) } - pub fn remove( - &self, - storage: &mut dyn Storage, - refund_offer: bool, - reward_payout: RewardPayout, - mut response: Response, - ) -> Result { - collection_offers().remove(storage, self.key())?; - - let return_address = self.order_info.asset_recipient(); - let mut funds_to_return = NativeBalance(vec![]); - - if refund_offer { - funds_to_return.add_assign(self.order_info.price.clone()); - } - - if let Some(expiration_info) = &self.order_info.expiration_info { - if expiration_info.removal_reward.amount > Uint128::zero() { - let reward = expiration_info.removal_reward.clone(); - match &reward_payout { - RewardPayout::Contract => {} - RewardPayout::Return => { - funds_to_return.add_assign(reward); - } - RewardPayout::Other(recipient) => { - response = transfer_coin(reward, recipient, response); - } - } - } - } - - if !funds_to_return.is_empty() { - funds_to_return.normalize(); - response = transfer_coins(funds_to_return.into_vec(), &return_address, response); - } - - Ok(response) + pub fn remove(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { + collection_offers().remove(storage, self.id.clone())?; + Ok(()) } - pub fn match_with_ask(&self, deps: Deps, _env: &Env) -> Result, ContractError> { - let top_ask = query_asks_by_price( + pub fn match_with_ask(&self, deps: Deps) -> Result, ContractError> { + let top_ask = query_asks_by_collection_denom( deps, self.collection.clone(), - self.order_info.price.denom.clone(), + self.details.price.denom.clone(), QueryOptions { descending: Some(false), limit: Some(1), min: None, - max: Some(QueryBound::Exclusive(AsksByPriceOffset { - token_id: "".to_string(), - amount: self.order_info.price.amount.u128() + 1, + max: Some(QueryBound::Exclusive(PriceOffset { + id: "".to_string(), + amount: self.details.price.amount.u128() + 1, })), }, )? @@ -402,4 +363,33 @@ impl CollectionOffer { Ok(top_ask) } + + pub fn get_event_attrs(&self, attr_keys: Vec<&str>) -> Vec { + let mut attributes = vec![]; + for attr_key in attr_keys { + let attr = match attr_key { + "id" => Some(attr("id", self.id.to_string())), + "creator" => Some(attr("creator", self.creator.to_string())), + "collection" => Some(attr("collection", self.collection.to_string())), + "price" => Some(attr("price", self.details.price.to_string())), + "asset_recipient" => self + .details + .asset_recipient + .as_ref() + .map(|asset_recipient| attr("asset_recipient", asset_recipient.to_string())), + "finder" => self + .details + .finder + .as_ref() + .map(|finder| attr("finder", finder.to_string())), + &_ => { + unreachable!("Invalid attr_key: {}", attr_key) + } + }; + if let Some(value) = attr { + attributes.push(value); + } + } + attributes + } } diff --git a/contracts/marketplace-v2/src/query.rs b/contracts/marketplace-v2/src/query.rs index 9423a614..1e5266a0 100644 --- a/contracts/marketplace-v2/src/query.rs +++ b/contracts/marketplace-v2/src/query.rs @@ -1,20 +1,13 @@ use crate::{ helpers::build_collection_token_index_str, - msg::{ - AsksByCollectionOffset, AsksByCreatorOffset, AsksByExpirationOffset, AsksByPriceOffset, - CollectionOffersByCollectionOffset, CollectionOffersByCreatorOffset, - CollectionOffersByExpirationOffset, CollectionOffersByPriceOffset, - OffersByCollectionOffset, OffersByCreatorOffset, OffersByExpirationOffset, - OffersByTokenPriceOffset, QueryMsg, - }, + msg::{PriceOffset, QueryMsg}, + orders::{Ask, CollectionOffer, Offer}, state::{ - asks, collection_offers, offers, Ask, CollectionOffer, Config, Denom, Offer, PriceRange, - TokenId, PRICE_RANGES, SUDO_PARAMS, + asks, collection_offers, offers, AllowDenoms, Config, Denom, OrderId, ALLOW_DENOMS, CONFIG, }, }; use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, StdResult}; -use cw_storage_plus::Bound; use sg_index_query::{QueryOptions, QueryOptionsInternal}; #[cfg(not(feature = "library"))] @@ -25,64 +18,32 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { let api = deps.api; match msg { - QueryMsg::Config {} => to_json_binary(&query_sudo_params(deps)?), - QueryMsg::PriceRange { denom } => to_json_binary(&query_price_range(deps, denom)?), - QueryMsg::PriceRanges { query_options } => to_json_binary(&query_price_ranges( - deps, - query_options.unwrap_or(QueryOptions::default()), - )?), - QueryMsg::Ask { - collection, - token_id, - } => to_json_binary(&query_ask(deps, api.addr_validate(&collection)?, token_id)?), - QueryMsg::AsksByCollection { - collection, - query_options, - } => to_json_binary(&query_asks_by_collection( - deps, - api.addr_validate(&collection)?, - query_options.unwrap_or(QueryOptions::default()), - )?), - QueryMsg::AsksByPrice { + QueryMsg::Config {} => to_json_binary(&query_config(deps)?), + QueryMsg::AllowDenoms {} => to_json_binary(&query_allow_denoms(deps)?), + QueryMsg::Ask(id) => to_json_binary(&query_asks(deps, vec![id])?.pop()), + QueryMsg::Asks(ids) => to_json_binary(&query_asks(deps, ids)?), + QueryMsg::AsksByCollectionDenom { collection, denom, query_options, - } => to_json_binary(&query_asks_by_price( + } => to_json_binary(&query_asks_by_collection_denom( deps, api.addr_validate(&collection)?, denom, query_options.unwrap_or(QueryOptions::default()), )?), - QueryMsg::AsksByCreator { + QueryMsg::AsksByCreatorCollection { creator, - query_options, - } => to_json_binary(&query_asks_by_creator( - deps, - api.addr_validate(&creator)?, - query_options.unwrap_or(QueryOptions::default()), - )?), - QueryMsg::AsksByExpiration { query_options } => to_json_binary(&query_asks_by_expiration( - deps, - query_options.unwrap_or(QueryOptions::default()), - )?), - QueryMsg::Offer { - collection, - token_id, - creator, - } => to_json_binary(&query_offer( - deps, - api.addr_validate(&collection)?, - token_id, - api.addr_validate(&creator)?, - )?), - QueryMsg::OffersByCollection { collection, query_options, - } => to_json_binary(&query_offers_by_collection( + } => to_json_binary(&query_asks_by_creator_collection( deps, + api.addr_validate(&creator)?, api.addr_validate(&collection)?, query_options.unwrap_or(QueryOptions::default()), )?), + QueryMsg::Offer(id) => to_json_binary(&query_offers(deps, vec![id])?.pop()), + QueryMsg::Offers(ids) => to_json_binary(&query_offers(deps, ids)?), QueryMsg::OffersByTokenPrice { collection, token_id, @@ -95,33 +56,20 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { denom, query_options.unwrap_or(QueryOptions::default()), )?), - QueryMsg::OffersByCreator { - creator, - query_options, - } => to_json_binary(&query_offers_by_creator( - deps, - api.addr_validate(&creator)?, - query_options.unwrap_or(QueryOptions::default()), - )?), - QueryMsg::OffersByExpiration { query_options } => to_json_binary( - &query_offers_by_expiration(deps, query_options.unwrap_or(QueryOptions::default()))?, - ), - QueryMsg::CollectionOffer { - collection, + QueryMsg::OffersByCreatorCollection { creator, - } => to_json_binary(&query_collection_offer( - deps, - api.addr_validate(&collection)?, - api.addr_validate(&creator)?, - )?), - QueryMsg::CollectionOffersByCollection { collection, query_options, - } => to_json_binary(&query_collection_offers_by_collection( + } => to_json_binary(&query_offers_by_creator_collection( deps, + api.addr_validate(&creator)?, api.addr_validate(&collection)?, query_options.unwrap_or(QueryOptions::default()), )?), + QueryMsg::CollectionOffer(id) => { + to_json_binary(&query_collection_offers(deps, vec![id])?.pop()) + } + QueryMsg::CollectionOffers(ids) => to_json_binary(&query_collection_offers(deps, ids)?), QueryMsg::CollectionOffersByPrice { collection, denom, @@ -132,104 +80,58 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { denom, query_options.unwrap_or(QueryOptions::default()), )?), - QueryMsg::CollectionOffersByCreator { + QueryMsg::CollectionOffersByCreatorCollection { creator, + collection, query_options, - } => to_json_binary(&query_collection_offers_by_creator( + } => to_json_binary(&query_collection_offers_by_creator_collection( deps, api.addr_validate(&creator)?, + api.addr_validate(&collection)?, query_options.unwrap_or(QueryOptions::default()), )?), - QueryMsg::CollectionOffersByExpiration { query_options } => { - to_json_binary(&query_collection_offers_by_expiration( - deps, - query_options.unwrap_or(QueryOptions::default()), - )?) - } } } -pub fn query_sudo_params(deps: Deps) -> StdResult> { - SUDO_PARAMS.load(deps.storage) +pub fn query_config(deps: Deps) -> StdResult> { + CONFIG.load(deps.storage) } -pub fn query_price_range(deps: Deps, denom: Denom) -> StdResult { - PRICE_RANGES.load(deps.storage, denom) +pub fn query_allow_denoms(deps: Deps) -> StdResult { + ALLOW_DENOMS.load(deps.storage) } -pub fn query_price_ranges( - deps: Deps, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack(&(|offset| offset.to_string()), None, None); +pub fn query_asks(deps: Deps, ids: Vec) -> StdResult> { + let mut retval = vec![]; - let denom_price_ranges = PRICE_RANGES - .range(deps.storage, min, max, order) - .take(limit) - .collect::>>()?; - - Ok(denom_price_ranges) -} - -pub fn query_ask(deps: Deps, collection: Addr, token_id: TokenId) -> StdResult> { - let ask = asks().may_load(deps.storage, Ask::build_key(&collection, &token_id))?; - Ok(ask) -} - -pub fn query_asks_by_collection( - deps: Deps, - collection: Addr, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack(&(|offset| offset.token_id.to_string()), None, None); - - let asks = asks() - .prefix(collection.to_string()) - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; + for id in ids { + let ask = asks().may_load(deps.storage, id)?; + if let Some(ask) = ask { + retval.push(ask); + } + } - Ok(asks) + Ok(retval) } -pub fn query_asks_by_price( +pub fn query_asks_by_collection_denom( deps: Deps, collection: Addr, denom: Denom, - query_options: QueryOptions, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| { - ( - offset.amount, - (collection.to_string(), offset.token_id.clone()), - ) - }), - None, - None, - ); + } = query_options.unpack(&(|offset| (offset.amount, offset.id.clone())), None, None); let results = asks() .idx .collection_denom_price .sub_prefix((collection, denom)) - .range_raw(deps.storage, min, max, order) + .range(deps.storage, min, max, order) .take(limit) .map(|res| res.map(|(_, ask)| ask)) .collect::>>()?; @@ -237,430 +139,157 @@ pub fn query_asks_by_price( Ok(results) } -pub fn query_asks_by_creator( +pub fn query_asks_by_creator_collection( deps: Deps, creator: Addr, - query_options: QueryOptions, + collection: Addr, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| (offset.collection.clone(), offset.token_id.clone())), - None, - None, - ); - - let asks = asks() - .idx - .creator - .prefix(creator) - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; + } = query_options.unpack(&(|offset| offset.clone()), None, None); - Ok(asks) -} - -pub fn query_asks_by_expiration( - deps: Deps, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack( - &(|offset| { - ( - offset.expiration, - (offset.collection.clone(), offset.token_id.clone()), - ) - }), - None, - None, - ); - - let max = Some(max.unwrap_or(Bound::exclusive(( - u64::MAX, - ("".to_string(), "".to_string()), - )))); - - let asks = asks() + let results = asks() .idx - .expiration + .creator_collection + .prefix((creator, collection)) .range(deps.storage, min, max, order) .take(limit) - .map(|res| res.map(|item| item.1)) + .map(|res| res.map(|(_, ask)| ask)) .collect::>>()?; - Ok(asks) + Ok(results) } -pub fn query_offer( - deps: Deps, - collection: Addr, - token_id: TokenId, - bidder: Addr, -) -> StdResult> { - let offer = offers().may_load( - deps.storage, - Offer::build_key(&collection, &token_id, &bidder), - )?; - Ok(offer) -} +pub fn query_offers(deps: Deps, ids: Vec) -> StdResult> { + let mut retval = vec![]; -pub fn query_offers_by_collection( - deps: Deps, - collection: Addr, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack( - &(|offset| { - ( - offset.token_id.clone(), - Addr::unchecked(offset.creator.clone()), - ) - }), - None, - None, - ); - - let offers = offers() - .sub_prefix(collection) - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; + for id in ids { + let offer = offers().may_load(deps.storage, id)?; + if let Some(offer) = offer { + retval.push(offer); + } + } - Ok(offers) + Ok(retval) } pub fn query_offers_by_token_price( deps: Deps, collection: Addr, - token_id: TokenId, + token_id: String, denom: Denom, - query_options: QueryOptions, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| { - ( - offset.amount, - ( - collection.clone(), - token_id.clone(), - Addr::unchecked(offset.creator.clone()), - ), - ) - }), - None, - None, - ); - - let collection_token_index_str = - build_collection_token_index_str(collection.as_ref(), &token_id); - - let offers = offers() + } = query_options.unpack(&(|offset| (offset.amount, offset.id.clone())), None, None); + + let results = offers() .idx .token_denom_price - .sub_prefix((collection_token_index_str, denom)) + .sub_prefix(( + build_collection_token_index_str(collection.as_ref(), &token_id), + denom, + )) .range(deps.storage, min, max, order) .take(limit) - .map(|res| res.map(|item| item.1)) + .map(|res| res.map(|(_, offer)| offer)) .collect::>>()?; - Ok(offers) + Ok(results) } -pub fn query_offers_by_creator( +pub fn query_offers_by_creator_collection( deps: Deps, creator: Addr, - query_options: QueryOptions, + collection: Addr, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| { - ( - Addr::unchecked(offset.collection.clone()), - offset.token_id.clone(), - creator.clone(), - ) - }), - None, - None, - ); - - let offers = offers() - .idx - .creator - .prefix(creator) - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; + } = query_options.unpack(&(|offset| offset.clone()), None, None); - Ok(offers) -} - -pub fn query_offers_by_expiration( - deps: Deps, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack( - &(|offset| { - ( - offset.expiration, - ( - Addr::unchecked(offset.collection.clone()), - offset.token_id.clone(), - Addr::unchecked(offset.creator.clone()), - ), - ) - }), - None, - None, - ); - - let max = Some(max.unwrap_or(Bound::exclusive(( - u64::MAX, - (Addr::unchecked(""), "".to_string(), Addr::unchecked("")), - )))); - - let offers = offers() + let results = offers() .idx - .expiration + .creator_collection + .prefix((creator, collection)) .range(deps.storage, min, max, order) .take(limit) - .map(|res| res.map(|item| item.1)) + .map(|res| res.map(|(_, offer)| offer)) .collect::>>()?; - Ok(offers) + Ok(results) } -pub fn query_collection_offer( - deps: Deps, - collection: Addr, - bidder: Addr, -) -> StdResult> { - let collection_offer = collection_offers().may_load( - deps.storage, - CollectionOffer::build_key(&collection, &bidder), - )?; - Ok(collection_offer) -} +pub fn query_collection_offers(deps: Deps, ids: Vec) -> StdResult> { + let mut retval = vec![]; -pub fn query_collection_offers_by_collection( - deps: Deps, - collection: Addr, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack( - &(|offset| Addr::unchecked(offset.creator.clone())), - None, - None, - ); - - let offers = collection_offers() - .prefix(collection) - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; + for id in ids { + let collection_offer = collection_offers().may_load(deps.storage, id)?; + if let Some(collection_offer) = collection_offer { + retval.push(collection_offer); + } + } - Ok(offers) + Ok(retval) } pub fn query_collection_offers_by_price( deps: Deps, collection: Addr, denom: Denom, - query_options: QueryOptions, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| { - ( - offset.amount, - (collection.clone(), Addr::unchecked(offset.creator.clone())), - ) - }), - None, - None, - ); - - let offers = collection_offers() + } = query_options.unpack(&(|offset| (offset.amount, offset.id.clone())), None, None); + + let results = collection_offers() .idx .collection_denom_price .sub_prefix((collection, denom)) .range(deps.storage, min, max, order) .take(limit) - .map(|res| res.map(|item| item.1)) + .map(|res| res.map(|(_, collection_offer)| collection_offer)) .collect::>>()?; - Ok(offers) + Ok(results) } -pub fn query_collection_offers_by_creator( +pub fn query_collection_offers_by_creator_collection( deps: Deps, creator: Addr, - query_options: QueryOptions, + collection: Addr, + query_options: QueryOptions, ) -> StdResult> { let QueryOptionsInternal { limit, order, min, max, - } = query_options.unpack( - &(|offset| (Addr::unchecked(offset.collection.clone()), creator.clone())), - None, - None, - ); + } = query_options.unpack(&(|offset| offset.clone()), None, None); - let offers = collection_offers() + let results = collection_offers() .idx - .creator - .prefix(creator) + .creator_collection + .prefix((creator, collection)) .range(deps.storage, min, max, order) .take(limit) - .map(|res| res.map(|item| item.1)) + .map(|res| res.map(|(_, collection_offer)| collection_offer)) .collect::>>()?; - Ok(offers) -} - -pub fn query_collection_offers_by_expiration( - deps: Deps, - query_options: QueryOptions, -) -> StdResult> { - let QueryOptionsInternal { - limit, - order, - min, - max, - } = query_options.unpack( - &(|offset| { - ( - offset.expiration, - ( - Addr::unchecked(offset.collection.clone()), - Addr::unchecked(offset.creator.clone()), - ), - ) - }), - None, - None, - ); - - let offers = collection_offers() - .idx - .expiration - .range(deps.storage, min, max, order) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; - - Ok(offers) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::state::OrderInfo; - - use cosmwasm_std::{coin, testing::mock_dependencies}; - use sg_index_query::QueryBound; - use sg_std::NATIVE_DENOM; - - #[test] - fn test_query_offers_by_token_price() { - let mut deps = mock_dependencies(); - // let mut env = mock_env(); - - let creator = Addr::unchecked("creator"); - let collection = Addr::unchecked("collection"); - let token_id = "1".to_string(); - - let offer = Offer { - collection: collection.clone(), - token_id: token_id.to_string(), - order_info: OrderInfo { - creator: creator.clone(), - price: coin(100, NATIVE_DENOM), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: None, - }, - }; - offer.save(&mut deps.storage).unwrap(); - - let result = query_offers_by_token_price( - deps.as_ref(), - collection.clone(), - token_id.clone(), - NATIVE_DENOM.to_string(), - QueryOptions { - limit: Some(1), - descending: Some(true), - min: Some(QueryBound::Inclusive(OffersByTokenPriceOffset { - creator: "".to_string(), - amount: 100u128, - })), - max: None, - }, - ) - .unwrap(); - assert!(result.len() == 1); - - let result = query_offers_by_token_price( - deps.as_ref(), - collection, - token_id, - NATIVE_DENOM.to_string(), - QueryOptions { - limit: Some(1), - descending: Some(true), - min: Some(QueryBound::Exclusive(OffersByTokenPriceOffset { - creator: "".to_string(), - amount: 101u128, - })), - max: None, - }, - ) - .unwrap(); - assert!(result.is_empty()); - } + Ok(results) } diff --git a/contracts/marketplace-v2/src/state.rs b/contracts/marketplace-v2/src/state.rs index 889966ee..7f57795d 100644 --- a/contracts/marketplace-v2/src/state.rs +++ b/contracts/marketplace-v2/src/state.rs @@ -1,322 +1,195 @@ -use crate::constants::MAX_BASIS_POINTS; -use crate::{helpers::build_collection_token_index_str, ContractError}; +use crate::helpers::build_collection_token_index_str; +use crate::orders::{CollectionOffer, Offer}; +use crate::ContractError; +use crate::{constants::MAX_BASIS_POINTS, orders::Ask}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Addr, Api, Coin, Storage, Timestamp, Uint128}; +use cosmwasm_std::{ensure, Addr, Api, Storage}; use cw_address_like::AddressLike; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; -use std::fmt; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex}; +pub type OrderId = String; pub type TokenId = String; pub type Denom = String; #[cw_serde] pub struct Config { - /// The address of the fair burn contract - pub fair_burn: T, + /// The address of the address that will receive the protocol fees + pub fee_manager: T, /// The address of the royalty registry contract pub royalty_registry: T, - /// Listing fee to reduce spam - pub listing_fee: Coin, - /// Minimum removal reward - pub min_removal_reward: Coin, - /// Fair Burn fee - pub trading_fee_bps: u64, + /// Protocol fee + pub protocol_fee_bps: u64, /// Max value for the royalty fee pub max_royalty_fee_bps: u64, - /// Max value for the finders fee - pub max_finders_fee_bps: u64, - /// Minimum expiry seconds for asks / offers - pub min_expiration_seconds: u64, - /// The number of seconds to look ahead for orders to remove - pub order_removal_lookahead_secs: u64, - /// The maximum number of asks that can be removed per block - pub max_asks_removed_per_block: u32, - /// The maximum number of offers that can be removed per block - pub max_offers_removed_per_block: u32, - /// The maximum number of collection offers that can be removed per block - pub max_collection_offers_removed_per_block: u32, + /// The reward paid out to the market maker. Reward is a percentage of the protocol fee + pub maker_reward_bps: u64, + /// The reward paid out to the market taker. Reward is a percentage of the protocol fee + pub taker_reward_bps: u64, } impl Config { pub fn str_to_addr(self, api: &dyn Api) -> Result, ContractError> { Ok(Config { - fair_burn: api.addr_validate(&self.fair_burn)?, + fee_manager: api.addr_validate(&self.fee_manager)?, royalty_registry: api.addr_validate(&self.royalty_registry)?, - listing_fee: self.listing_fee, - min_removal_reward: self.min_removal_reward, - trading_fee_bps: self.trading_fee_bps, + protocol_fee_bps: self.protocol_fee_bps, max_royalty_fee_bps: self.max_royalty_fee_bps, - max_finders_fee_bps: self.max_finders_fee_bps, - min_expiration_seconds: self.min_expiration_seconds, - order_removal_lookahead_secs: self.order_removal_lookahead_secs, - max_asks_removed_per_block: self.max_asks_removed_per_block, - max_offers_removed_per_block: self.max_offers_removed_per_block, - max_collection_offers_removed_per_block: self.max_collection_offers_removed_per_block, + maker_reward_bps: self.maker_reward_bps, + taker_reward_bps: self.taker_reward_bps, }) } } impl Config { pub fn save(&self, storage: &mut dyn Storage) -> Result<(), ContractError> { - SUDO_PARAMS.save(storage, self)?; - Ok(()) - } - - pub fn validate(&self) -> Result<(), ContractError> { ensure!( - self.trading_fee_bps < MAX_BASIS_POINTS, + self.protocol_fee_bps < MAX_BASIS_POINTS, ContractError::InvalidInput("trade_fee_bps must be less than 1".to_string()) ); ensure!( - self.max_finders_fee_bps < MAX_BASIS_POINTS, - ContractError::InvalidInput("max_finders_fee_bps must be less than 1".to_string()) + self.maker_reward_bps < MAX_BASIS_POINTS, + ContractError::InvalidInput("maker_reward_bps must be less than 1".to_string()) ); - + ensure!( + self.taker_reward_bps < MAX_BASIS_POINTS, + ContractError::InvalidInput("taker_reward_bps must be less than 1".to_string()) + ); + CONFIG.save(storage, self)?; Ok(()) } } -pub const SUDO_PARAMS: Item> = Item::new("S"); +pub const CONFIG: Item> = Item::new("C"); -// A map of acceptable denoms to their minimum trade prices. -// Denoms not found in the Map are not accepted. #[cw_serde] -pub struct PriceRange { - pub min: Uint128, - pub max: Uint128, +pub enum AllowDenoms { + Includes(Vec), + Excludes(Vec), } -pub const PRICE_RANGES: Map = Map::new("P"); -impl fmt::Display for PriceRange { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{{\"min\":{},\"max\":{}}}", self.min, self.max) +impl AllowDenoms { + pub fn contains(&self, denom: &Denom) -> bool { + match self { + AllowDenoms::Includes(denoms) => denoms.contains(denom), + AllowDenoms::Excludes(denoms) => !denoms.contains(denom), + } } } -#[cw_serde] -pub struct ExpirationInfo { - pub expiration: Timestamp, - pub removal_reward: Coin, -} +pub const ALLOW_DENOMS: Item = Item::new("D"); -#[cw_serde] -pub struct OrderInfo { - pub price: Coin, - pub creator: Addr, - pub asset_recipient: Option, - pub finders_fee_bps: Option, - pub expiration_info: Option, -} - -pub trait KeyString { - fn to_string(&self) -> String; -} - -/// Primary key for asks: (collection, token_id) -pub type AskKey = (String, String); - -impl KeyString for AskKey { - fn to_string(&self) -> String { - format!("{}-{}", self.0, self.1) - } -} - -/// Represents an ask on the marketplace -#[cw_serde] -pub struct Ask { - pub collection: Addr, - pub token_id: String, - pub order_info: OrderInfo, -} +pub const NONCE: Item = Item::new("N"); /// Defines indices for accessing Asks pub struct AskIndices<'a> { // Index Asks by collection and denom price - pub collection_denom_price: MultiIndex<'a, (Addr, Denom, u128), Ask, AskKey>, - // Index Asks by creator - pub creator: MultiIndex<'a, Addr, Ask, AskKey>, - // Index asks by expiration (seconds), necessary to remove in EndBlocker - pub expiration: MultiIndex<'a, u64, Ask, AskKey>, + pub collection_denom_price: MultiIndex<'a, (Addr, Denom, u128), Ask, OrderId>, + // Index Asks by creator and collection + pub creator_collection: MultiIndex<'a, (Addr, Addr), Ask, OrderId>, } impl<'a> IndexList for AskIndices<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![ - &self.collection_denom_price, - &self.creator, - &self.expiration, - ]; + let v: Vec<&dyn Index> = vec![&self.collection_denom_price, &self.creator_collection]; Box::new(v.into_iter()) } } -pub fn asks<'a>() -> IndexedMap<'a, AskKey, Ask, AskIndices<'a>> { +pub fn asks<'a>() -> IndexedMap<'a, OrderId, Ask, AskIndices<'a>> { let indexes: AskIndices = AskIndices { collection_denom_price: MultiIndex::new( |_pk: &[u8], a: &Ask| { ( a.collection.clone(), - a.order_info.price.denom.clone(), - a.order_info.price.amount.u128(), + a.details.price.denom.clone(), + a.details.price.amount.u128(), ) }, - "A", - "Ap", + "a", + "a_p", ), - creator: MultiIndex::new( - |_pk: &[u8], a: &Ask| a.order_info.creator.clone(), - "A", - "Ac", - ), - expiration: MultiIndex::new( - |_pk: &[u8], a: &Ask| { - a.order_info - .expiration_info - .as_ref() - .map_or(u64::MAX, |e| e.expiration.seconds()) - }, - "A", - "Ae", + creator_collection: MultiIndex::new( + |_pk: &[u8], a: &Ask| (a.creator.clone(), a.collection.clone()), + "a", + "a_c", ), }; - IndexedMap::new("A", indexes) -} - -/// Primary key for offers: (collection, token_id, creator) -pub type OfferKey = (Addr, TokenId, Addr); - -impl KeyString for OfferKey { - fn to_string(&self) -> String { - format!("{}-{}-{}", self.0, self.1, self.2) - } -} - -/// Represents an offer on an NFT in the marketplace -#[cw_serde] -pub struct Offer { - pub collection: Addr, - pub token_id: String, - pub order_info: OrderInfo, + IndexedMap::new("a", indexes) } /// Defines incides for accessing offers pub struct OfferIndices<'a> { - // Index offers for a token id, sorted by denom price (infinity dependency) - pub token_denom_price: MultiIndex<'a, (TokenId, Denom, u128), Offer, OfferKey>, - // Index offers by creator - pub creator: MultiIndex<'a, Addr, Offer, OfferKey>, - // Index offers by expiration - pub expiration: MultiIndex<'a, u64, Offer, OfferKey>, + // Index offers for a token id, sorted by denom price (infinity router dependency) + pub token_denom_price: MultiIndex<'a, (TokenId, Denom, u128), Offer, OrderId>, + // Index offers by creator and collection + pub creator_collection: MultiIndex<'a, (Addr, Addr), Offer, OrderId>, } impl<'a> IndexList for OfferIndices<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = - vec![&self.token_denom_price, &self.creator, &self.expiration]; + let v: Vec<&dyn Index> = vec![&self.token_denom_price, &self.creator_collection]; Box::new(v.into_iter()) } } -pub fn offers<'a>() -> IndexedMap<'a, OfferKey, Offer, OfferIndices<'a>> { +pub fn offers<'a>() -> IndexedMap<'a, OrderId, Offer, OfferIndices<'a>> { let indexes = OfferIndices { token_denom_price: MultiIndex::new( |_pk: &[u8], o: &Offer| { ( build_collection_token_index_str(o.collection.as_ref(), &o.token_id), - o.order_info.price.denom.clone(), - o.order_info.price.amount.u128(), + o.details.price.denom.clone(), + o.details.price.amount.u128(), ) }, - "O", - "Op", - ), - creator: MultiIndex::new( - |_pk: &[u8], o: &Offer| o.order_info.creator.clone(), - "O", - "Oc", + "o", + "o_p", ), - expiration: MultiIndex::new( - |_pk: &[u8], o: &Offer| { - o.order_info - .expiration_info - .as_ref() - .map_or(u64::MAX, |e| e.expiration.seconds()) - }, - "O", - "Oe", + creator_collection: MultiIndex::new( + |_pk: &[u8], o: &Offer| (o.creator.clone(), o.collection.clone()), + "o", + "o_c", ), }; - IndexedMap::new("O", indexes) -} - -/// Primary key for collection offers: (collection, creator) -pub type CollectionOfferKey = (Addr, Addr); - -impl KeyString for CollectionOfferKey { - fn to_string(&self) -> String { - format!("{}-{}", self.0, self.1) - } -} - -/// Represents an offer across an entire collection in the marketplace -#[cw_serde] -pub struct CollectionOffer { - pub collection: Addr, - pub order_info: OrderInfo, + IndexedMap::new("o", indexes) } /// Defines incides for accessing collection offers pub struct CollectionOfferIndices<'a> { // Index collection offers by collection and price - pub collection_denom_price: - MultiIndex<'a, (Addr, Denom, u128), CollectionOffer, CollectionOfferKey>, + pub collection_denom_price: MultiIndex<'a, (Addr, Denom, u128), CollectionOffer, OrderId>, // Index collection offers by creator - pub creator: MultiIndex<'a, Addr, CollectionOffer, CollectionOfferKey>, - // Index collections by expiration, necessary to remove in endblocker - pub expiration: MultiIndex<'a, u64, CollectionOffer, CollectionOfferKey>, + pub creator_collection: MultiIndex<'a, (Addr, Addr), CollectionOffer, OrderId>, } impl<'a> IndexList for CollectionOfferIndices<'a> { fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![ - &self.collection_denom_price, - &self.creator, - &self.expiration, - ]; + let v: Vec<&dyn Index> = + vec![&self.collection_denom_price, &self.creator_collection]; Box::new(v.into_iter()) } } pub fn collection_offers<'a>( -) -> IndexedMap<'a, CollectionOfferKey, CollectionOffer, CollectionOfferIndices<'a>> { +) -> IndexedMap<'a, OrderId, CollectionOffer, CollectionOfferIndices<'a>> { let indexes = CollectionOfferIndices { collection_denom_price: MultiIndex::new( |_pk: &[u8], co: &CollectionOffer| { ( co.collection.clone(), - co.order_info.price.denom.clone(), - co.order_info.price.amount.u128(), + co.details.price.denom.clone(), + co.details.price.amount.u128(), ) }, - "C", - "Cp", - ), - creator: MultiIndex::new( - |_pk: &[u8], co: &CollectionOffer| co.order_info.creator.clone(), - "C", - "Cc", + "c", + "c_p", ), - expiration: MultiIndex::new( - |_pk: &[u8], co: &CollectionOffer| { - co.order_info - .expiration_info - .as_ref() - .map_or(u64::MAX, |e| e.expiration.seconds()) - }, - "C", - "Ce", + creator_collection: MultiIndex::new( + |_pk: &[u8], co: &CollectionOffer| (co.creator.clone(), co.collection.clone()), + "c", + "c_c", ), }; - IndexedMap::new("C", indexes) + IndexedMap::new("c", indexes) } diff --git a/contracts/marketplace-v2/src/sudo.rs b/contracts/marketplace-v2/src/sudo.rs deleted file mode 100644 index 05703fb8..00000000 --- a/contracts/marketplace-v2/src/sudo.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::{ - msg::{ - AsksByExpirationOffset, CollectionOffersByExpirationOffset, OffersByExpirationOffset, - SudoMsg, - }, - orders::RewardPayout, - query::{ - query_asks_by_expiration, query_collection_offers_by_expiration, query_offers_by_expiration, - }, - state::{Denom, PriceRange, PRICE_RANGES, SUDO_PARAMS}, - ContractError, -}; - -use cosmwasm_std::{Coin, DepsMut, Env, Event}; -use cw_utils::NativeBalance; -use sg_index_query::{QueryBound, QueryOptions}; -use sg_std::Response; -use stargaze_fair_burn::append_fair_burn_msg; -use std::ops::AddAssign; - -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn sudo(deps: DepsMut, env: Env, msg: SudoMsg) -> Result { - let _api = deps.api; - - match msg { - SudoMsg::BeginBlock {} => sudo_begin_block(deps, env), - SudoMsg::EndBlock {} => sudo_end_block(deps, env), - SudoMsg::UpdateParams { - fair_burn, - listing_fee, - min_removal_reward, - trading_fee_bps, - max_royalty_fee_bps, - max_finders_fee_bps, - min_expiration_seconds, - max_asks_removed_per_block, - max_offers_removed_per_block, - max_collection_offers_removed_per_block, - } => sudo_update_params( - deps, - env, - fair_burn, - listing_fee, - min_removal_reward, - trading_fee_bps, - max_royalty_fee_bps, - max_finders_fee_bps, - min_expiration_seconds, - max_asks_removed_per_block, - max_offers_removed_per_block, - max_collection_offers_removed_per_block, - ), - SudoMsg::AddDenoms { price_ranges } => sudo_add_denoms(deps, price_ranges), - SudoMsg::RemoveDenoms { denoms } => sudo_remove_denoms(deps, denoms), - } -} - -pub fn sudo_begin_block(_deps: DepsMut, _env: Env) -> Result { - Ok(Response::new()) -} - -pub fn sudo_end_block(deps: DepsMut, env: Env) -> Result { - let sudo_params = SUDO_PARAMS.load(deps.storage)?; - - let mut response = Response::new(); - let mut funds_fair_burn = NativeBalance::default(); - - // Fetch asks to remove - let expired_asks = query_asks_by_expiration( - deps.as_ref(), - QueryOptions { - descending: Some(false), - limit: Some(sudo_params.max_asks_removed_per_block), - min: None, - max: Some(QueryBound::Exclusive(AsksByExpirationOffset { - expiration: env - .block - .time - .plus_seconds(sudo_params.order_removal_lookahead_secs) - .plus_seconds(1) - .seconds(), - collection: "".to_string(), - token_id: "".to_string(), - })), - }, - )?; - - for ask in expired_asks { - if let Some(expiration_info) = &ask.order_info.expiration_info { - funds_fair_burn.add_assign(expiration_info.removal_reward.clone()); - } - - response = ask.remove(deps.storage, true, RewardPayout::Contract, response)?; - } - - // Remove expired offers - let expired_offers = query_offers_by_expiration( - deps.as_ref(), - QueryOptions { - descending: Some(false), - limit: Some(sudo_params.max_offers_removed_per_block), - min: None, - max: Some(QueryBound::Exclusive(OffersByExpirationOffset { - expiration: env - .block - .time - .plus_seconds(sudo_params.order_removal_lookahead_secs) - .plus_seconds(1) - .seconds(), - collection: "".to_string(), - token_id: "".to_string(), - creator: "".to_string(), - })), - }, - )?; - - for offer in expired_offers { - if let Some(expiration_info) = &offer.order_info.expiration_info { - funds_fair_burn.add_assign(expiration_info.removal_reward.clone()); - } - - response = offer.remove(deps.storage, true, RewardPayout::Contract, response)?; - } - - // Remove expired collection offers - let expired_collection_offers = query_collection_offers_by_expiration( - deps.as_ref(), - QueryOptions { - descending: Some(false), - limit: Some(sudo_params.max_offers_removed_per_block), - min: None, - max: Some(QueryBound::Exclusive(CollectionOffersByExpirationOffset { - expiration: env - .block - .time - .plus_seconds(sudo_params.order_removal_lookahead_secs) - .plus_seconds(1) - .seconds(), - collection: "".to_string(), - creator: "".to_string(), - })), - }, - )?; - - for collection_offer in expired_collection_offers { - if let Some(expiration_info) = &collection_offer.order_info.expiration_info { - funds_fair_burn.add_assign(expiration_info.removal_reward.clone()); - } - - response = collection_offer.remove(deps.storage, true, RewardPayout::Contract, response)?; - } - - funds_fair_burn.normalize(); - if !funds_fair_burn.is_empty() { - response = append_fair_burn_msg( - &sudo_params.fair_burn, - funds_fair_burn.into_vec(), - None, - response, - ); - } - - Ok(response) -} - -/// Only governance can update contract params -pub fn sudo_update_params( - deps: DepsMut, - _env: Env, - fair_burn: Option, - listing_fee: Option, - min_removal_reward: Option, - trading_fee_bps: Option, - max_royalty_fee_bps: Option, - max_finders_fee_bps: Option, - min_expiration_seconds: Option, - max_asks_removed_per_block: Option, - max_offers_removed_per_block: Option, - max_collection_offers_removed_per_block: Option, -) -> Result { - let mut sudo_params = SUDO_PARAMS.load(deps.storage)?; - let mut event = Event::new("sudo-update-params"); - - if let Some(fair_burn) = fair_burn { - event = event.add_attribute("fair_burn", fair_burn.to_string()); - sudo_params.fair_burn = deps.api.addr_validate(&fair_burn)?; - } - - if let Some(listing_fee) = listing_fee { - event = event.add_attribute("listing_fee", listing_fee.to_string()); - sudo_params.listing_fee = listing_fee; - } - - if let Some(min_removal_reward) = min_removal_reward { - event = event.add_attribute("min_removal_reward", min_removal_reward.to_string()); - sudo_params.min_removal_reward = min_removal_reward; - } - - if let Some(trading_fee_bps) = trading_fee_bps { - event = event.add_attribute("trading_fee_bps", sudo_params.trading_fee_bps.to_string()); - sudo_params.trading_fee_bps = trading_fee_bps; - } - - if let Some(max_royalty_fee_bps) = max_royalty_fee_bps { - event = event.add_attribute( - "max_royalty_fee_bps", - sudo_params.max_royalty_fee_bps.to_string(), - ); - sudo_params.max_royalty_fee_bps = max_royalty_fee_bps; - } - - if let Some(max_finders_fee_bps) = max_finders_fee_bps { - event = event.add_attribute( - "max_finders_fee_bps", - sudo_params.max_finders_fee_bps.to_string(), - ); - sudo_params.max_finders_fee_bps = max_finders_fee_bps; - } - - if let Some(min_expiration_seconds) = min_expiration_seconds { - event = event.add_attribute( - "min_expiration_seconds", - sudo_params.min_expiration_seconds.to_string(), - ); - sudo_params.min_expiration_seconds = min_expiration_seconds; - } - - if let Some(max_asks_removed_per_block) = max_asks_removed_per_block { - event = event.add_attribute( - "max_asks_removed_per_block", - max_asks_removed_per_block.to_string(), - ); - sudo_params.max_asks_removed_per_block = max_asks_removed_per_block; - } - - if let Some(max_offers_removed_per_block) = max_offers_removed_per_block { - event = event.add_attribute( - "max_offers_removed_per_block", - max_offers_removed_per_block.to_string(), - ); - sudo_params.max_offers_removed_per_block = max_offers_removed_per_block; - } - - if let Some(max_collection_offers_removed_per_block) = max_collection_offers_removed_per_block { - event = event.add_attribute( - "max_collection_offers_removed_per_block", - max_collection_offers_removed_per_block.to_string(), - ); - sudo_params.max_collection_offers_removed_per_block = - max_collection_offers_removed_per_block; - } - - sudo_params.validate()?; - sudo_params.save(deps.storage)?; - - Ok(Response::new().add_event(event)) -} - -pub fn sudo_add_denoms( - deps: DepsMut, - price_ranges: Vec<(Denom, PriceRange)>, -) -> Result { - let mut event = Event::new("sudo-add-denoms"); - - for (denom, price_range) in price_ranges { - PRICE_RANGES.save(deps.storage, denom.clone(), &price_range)?; - event = event.add_attribute(denom, price_range.to_string()); - } - - Ok(Response::new().add_event(event)) -} - -pub fn sudo_remove_denoms(deps: DepsMut, denoms: Vec) -> Result { - let mut event = Event::new("sudo-remove-denoms"); - - for denom in denoms { - PRICE_RANGES.remove(deps.storage, denom.clone()); - event = event.add_attribute("denom", denom); - } - - Ok(Response::new().add_event(event)) -} diff --git a/contracts/marketplace-v2/src/testing/helpers/marketplace.rs b/contracts/marketplace-v2/src/testing/helpers/marketplace.rs index dfccd3d4..edc0074b 100644 --- a/contracts/marketplace-v2/src/testing/helpers/marketplace.rs +++ b/contracts/marketplace-v2/src/testing/helpers/marketplace.rs @@ -1,24 +1,24 @@ use crate::{ - msg::{ExecuteMsg, OrderOptions}, + msg::ExecuteMsg, + orders::OrderDetails, testing::helpers::nft_functions::{approve, mint_for}, }; use cosmwasm_std::{Addr, Coin}; use cw_multi_test::Executor; -use sg_multi_test::StargazeApp; +use test_suite::common_setup::contract_boxes::App; #[allow(clippy::too_many_arguments)] pub fn mint_and_set_ask( - router: &mut StargazeApp, + router: &mut App, creator: &Addr, owner: &Addr, minter: &Addr, marketplace: &Addr, collection: &Addr, token_id: &str, - price: &Coin, send_funds: &[Coin], - order_options: Option>, + details: OrderDetails, ) { mint_for(router, creator, owner, minter, token_id); approve(router, owner, collection, marketplace, token_id); @@ -26,8 +26,7 @@ pub fn mint_and_set_ask( let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: price.clone(), - order_options, + details, }; let response = diff --git a/contracts/marketplace-v2/src/testing/helpers/nft_functions.rs b/contracts/marketplace-v2/src/testing/helpers/nft_functions.rs index abae0e02..bd1182f5 100644 --- a/contracts/marketplace-v2/src/testing/helpers/nft_functions.rs +++ b/contracts/marketplace-v2/src/testing/helpers/nft_functions.rs @@ -3,13 +3,13 @@ use cosmwasm_std::{Addr, Empty}; use cw_multi_test::{AppResponse, Executor}; use sg721::ExecuteMsg as Sg721ExecuteMsg; use sg721_base::msg::CollectionInfoResponse; -use sg_multi_test::StargazeApp; use sg_std::NATIVE_DENOM; +use test_suite::common_setup::contract_boxes::App; pub const _MINT_PRICE: u128 = 100_000_000; // Mints an NFT for a creator -pub fn _mint(router: &mut StargazeApp, creator: &Addr, minter_addr: &Addr) -> String { +pub fn _mint(router: &mut App, creator: &Addr, minter_addr: &Addr) -> String { let minter_msg = vending_minter::msg::ExecuteMsg::Mint {}; let response = router .execute_contract( @@ -32,7 +32,7 @@ pub fn _mint(router: &mut StargazeApp, creator: &Addr, minter_addr: &Addr) -> St } pub fn mint_for( - router: &mut StargazeApp, + router: &mut App, creator: &Addr, owner: &Addr, minter_addr: &Addr, @@ -52,7 +52,7 @@ pub fn mint_for( } pub fn approve( - router: &mut StargazeApp, + router: &mut App, creator: &Addr, collection: &Addr, marketplace: &Addr, @@ -67,8 +67,18 @@ pub fn approve( assert!(res.is_ok()); } +pub fn _approve_all(router: &mut App, creator: &Addr, collection: &Addr, marketplace: &Addr) { + let approve_all_msg: Sg721ExecuteMsg = + Sg721ExecuteMsg::ApproveAll { + operator: marketplace.to_string(), + expires: None, + }; + let res = router.execute_contract(creator.clone(), collection.clone(), &approve_all_msg, &[]); + assert!(res.is_ok()); +} + pub fn _transfer( - router: &mut StargazeApp, + router: &mut App, creator: &Addr, recipient: &Addr, collection: &Addr, diff --git a/contracts/marketplace-v2/src/testing/helpers/utils.rs b/contracts/marketplace-v2/src/testing/helpers/utils.rs index e0f08cab..f684ea38 100644 --- a/contracts/marketplace-v2/src/testing/helpers/utils.rs +++ b/contracts/marketplace-v2/src/testing/helpers/utils.rs @@ -1,9 +1,16 @@ use anyhow::Error; use cw_multi_test::AppResponse; -pub fn assert_error(response: Result, expected: String) { - assert_eq!( - response.unwrap_err().source().unwrap().to_string(), - expected - ); +pub fn assert_error(result: Result, expected: String) { + assert_eq!(result.unwrap_err().source().unwrap().to_string(), expected); +} + +pub fn find_attrs(response: AppResponse, event_ty: &str, attr_key: &str) -> Vec { + let mut values: Vec = vec![]; + for event in response.events.iter().filter(|x| x.ty == event_ty) { + for attr in event.attributes.iter().filter(|x| x.key == attr_key) { + values.push(attr.value.clone()); + } + } + values } diff --git a/contracts/marketplace-v2/src/testing/setup/msg.rs b/contracts/marketplace-v2/src/testing/setup/msg.rs index 74378bc3..800e9da6 100644 --- a/contracts/marketplace-v2/src/testing/setup/msg.rs +++ b/contracts/marketplace-v2/src/testing/setup/msg.rs @@ -1,6 +1,8 @@ use cosmwasm_std::Addr; + pub struct MarketAccounts { + pub creator: Addr, pub owner: Addr, pub bidder: Addr, - pub creator: Addr, + pub fee_manager: Addr, } diff --git a/contracts/marketplace-v2/src/testing/setup/setup_accounts.rs b/contracts/marketplace-v2/src/testing/setup/setup_accounts.rs index 321ae3ba..597456fe 100644 --- a/contracts/marketplace-v2/src/testing/setup/setup_accounts.rs +++ b/contracts/marketplace-v2/src/testing/setup/setup_accounts.rs @@ -3,17 +3,18 @@ use crate::testing::setup::setup_marketplace::{ATOM_DENOM, JUNO_DENOM}; use cosmwasm_std::{coin, coins, Addr, Coin}; use cw_multi_test::SudoMsg as CwSudoMsg; use cw_multi_test::{BankSudo, SudoMsg}; -use sg_multi_test::StargazeApp; use sg_std::NATIVE_DENOM; +use test_suite::common_setup::contract_boxes::App; // all amounts in ustars pub const INITIAL_BALANCE: u128 = 5_000_000_000; // initializes accounts with balances -pub fn setup_accounts(router: &mut StargazeApp) -> Result<(Addr, Addr, Addr), ContractError> { +pub fn setup_accounts(router: &mut App) -> Result<(Addr, Addr, Addr, Addr), ContractError> { + let creator: Addr = Addr::unchecked("creator"); let owner: Addr = Addr::unchecked("owner"); let bidder: Addr = Addr::unchecked("bidder"); - let creator: Addr = Addr::unchecked("creator"); + let fee_manager: Addr = Addr::unchecked("fee_manager"); let funds: Vec = vec![ coin(INITIAL_BALANCE, ATOM_DENOM), coin(INITIAL_BALANCE, JUNO_DENOM), @@ -55,13 +56,10 @@ pub fn setup_accounts(router: &mut StargazeApp) -> Result<(Addr, Addr, Addr), Co let creator_native_balances = router.wrap().query_all_balances(creator.clone()).unwrap(); assert_eq!(creator_native_balances, funds); - Ok((owner, bidder, creator)) + Ok((creator, owner, bidder, fee_manager)) } -pub fn setup_additional_account( - router: &mut StargazeApp, - addr_input: &str, -) -> Result { +pub fn setup_additional_account(router: &mut App, addr_input: &str) -> Result { let addr: Addr = Addr::unchecked(addr_input); let funds: Vec = coins(INITIAL_BALANCE, NATIVE_DENOM); router diff --git a/contracts/marketplace-v2/src/testing/setup/setup_contracts.rs b/contracts/marketplace-v2/src/testing/setup/setup_contracts.rs index b281649e..17e6defe 100644 --- a/contracts/marketplace-v2/src/testing/setup/setup_contracts.rs +++ b/contracts/marketplace-v2/src/testing/setup/setup_contracts.rs @@ -1,38 +1,12 @@ use crate::ContractError; -use cosmwasm_std::{Addr, Decimal}; +use cosmwasm_std::{Addr, Decimal, Empty}; use cw_multi_test::{Contract, ContractWrapper, Executor}; -use sg_multi_test::StargazeApp; -use sg_std::StargazeMsgWrapper; -use stargaze_fair_burn::msg::InstantiateMsg as FairBurnInstantiateMsg; use stargaze_royalty_registry::msg::InstantiateMsg as RoyaltyRegistryInstantiateMsg; use stargaze_royalty_registry::state::Config as RoyaltyRegistryConfig; +use test_suite::common_setup::contract_boxes::App; -pub fn contract_fair_burn() -> Box> { - let contract = ContractWrapper::new( - stargaze_fair_burn::contract::execute, - stargaze_fair_burn::contract::instantiate, - stargaze_fair_burn::contract::query, - ); - Box::new(contract) -} - -pub fn setup_fair_burn(router: &mut StargazeApp, creator: &Addr) -> Result { - let fair_burn_id = router.store_code(contract_fair_burn()); - let fair_burn = router - .instantiate_contract( - fair_burn_id, - creator.clone(), - &FairBurnInstantiateMsg { fee_bps: 5000 }, - &[], - "FairBurn", - None, - ) - .unwrap(); - Ok(fair_burn) -} - -pub fn contract_royalty_registry() -> Box> { +pub fn contract_royalty_registry() -> Box> { let contract = ContractWrapper::new( stargaze_royalty_registry::execute::execute, stargaze_royalty_registry::instantiate::instantiate, @@ -41,10 +15,7 @@ pub fn contract_royalty_registry() -> Box> { Box::new(contract) } -pub fn setup_royalty_registry( - router: &mut StargazeApp, - creator: &Addr, -) -> Result { +pub fn setup_royalty_registry(router: &mut App, creator: &Addr) -> Result { let royalty_registry_id = router.store_code(contract_royalty_registry()); let royalty_registry = router .instantiate_contract( @@ -64,13 +35,12 @@ pub fn setup_royalty_registry( Ok(royalty_registry) } -pub fn contract_marketplace() -> Box> { +pub fn contract_marketplace() -> Box> { let contract = ContractWrapper::new( crate::execute::execute, crate::instantiate::instantiate, crate::query::query, ) - .with_sudo(crate::sudo::sudo) .with_migrate(crate::migrate::migrate); Box::new(contract) } diff --git a/contracts/marketplace-v2/src/testing/setup/setup_marketplace.rs b/contracts/marketplace-v2/src/testing/setup/setup_marketplace.rs index 12ab0e6c..b63f8206 100644 --- a/contracts/marketplace-v2/src/testing/setup/setup_marketplace.rs +++ b/contracts/marketplace-v2/src/testing/setup/setup_marketplace.rs @@ -1,63 +1,45 @@ -use cosmwasm_std::{coin, Addr, Uint128}; +use crate::{ + msg::InstantiateMsg, state::Config, testing::setup::setup_contracts::contract_marketplace, + ContractError, +}; + +use cosmwasm_std::Addr; use cw_multi_test::Executor; -use sg_multi_test::StargazeApp; use sg_std::NATIVE_DENOM; - -use crate::error::ContractError; -use crate::msg::InstantiateMsg; -use crate::state::{Config, PriceRange}; -use crate::testing::setup::setup_contracts::contract_marketplace; +use test_suite::common_setup::contract_boxes::App; pub const ATOM_DENOM: &str = "uatom"; pub const JUNO_DENOM: &str = "ujuno"; pub fn setup_marketplace( - router: &mut StargazeApp, - fair_burn: Addr, + router: &mut App, + fee_manager: Addr, royalty_registry: Addr, marketplace_admin: Addr, ) -> Result { let marketplace_id = router.store_code(contract_marketplace()); let msg = InstantiateMsg { config: Config { - fair_burn: fair_burn.to_string(), + fee_manager: fee_manager.to_string(), royalty_registry: royalty_registry.to_string(), - listing_fee: coin(1_000_000, NATIVE_DENOM), - min_removal_reward: coin(4_000_000, NATIVE_DENOM), - trading_fee_bps: 200, + protocol_fee_bps: 200, max_royalty_fee_bps: 1000, - max_finders_fee_bps: 400, - min_expiration_seconds: 600, - order_removal_lookahead_secs: 10, - max_asks_removed_per_block: 50, - max_offers_removed_per_block: 20, - max_collection_offers_removed_per_block: 30, + maker_reward_bps: 4000, + taker_reward_bps: 1000, }, - price_ranges: vec![ - ( - NATIVE_DENOM.to_string(), - PriceRange { - min: Uint128::from(100_000u128), - max: Uint128::from(100_000_000_000_000u128), - }, - ), - ( - ATOM_DENOM.to_string(), - PriceRange { - min: Uint128::from(100_000u128), - max: Uint128::from(100_000_000_000_000u128), - }, - ), - ], + allow_denoms: crate::state::AllowDenoms::Includes(vec![ + NATIVE_DENOM.to_string(), + ATOM_DENOM.to_string(), + ]), }; let marketplace = router .instantiate_contract( marketplace_id, - marketplace_admin, + marketplace_admin.clone(), &msg, &[], "Marketplace", - None, + Some(marketplace_admin.to_string()), ) .unwrap(); Ok(marketplace) diff --git a/contracts/marketplace-v2/src/testing/setup/templates.rs b/contracts/marketplace-v2/src/testing/setup/templates.rs index bf51efb2..cbfa462d 100644 --- a/contracts/marketplace-v2/src/testing/setup/templates.rs +++ b/contracts/marketplace-v2/src/testing/setup/templates.rs @@ -1,7 +1,5 @@ use crate::testing::setup::{ - msg::MarketAccounts, - setup_accounts::setup_accounts, - setup_contracts::{setup_fair_burn, setup_royalty_registry}, + msg::MarketAccounts, setup_accounts::setup_accounts, setup_contracts::setup_royalty_registry, setup_marketplace::setup_marketplace, }; @@ -11,6 +9,7 @@ use sg_std::GENESIS_MINT_START_TIME; use test_suite::common_setup::{ contract_boxes::custom_mock_app, msg::{MinterCollectionResponse, MinterTemplateResponse}, + setup_accounts_and_block::setup_block_time, setup_minter::{ common::minter_params::minter_params_token, vending_minter::setup::{configure_minter, vending_minter_code_ids}, @@ -19,7 +18,7 @@ use test_suite::common_setup::{ pub fn standard_minter_template(num_tokens: u32) -> MinterTemplateResponse { let mut app = custom_mock_app(); - let (owner, bidder, creator) = setup_accounts(&mut app).unwrap(); + let (creator, owner, bidder, fee_manager) = setup_accounts(&mut app).unwrap(); let start_time = Timestamp::from_nanos(GENESIS_MINT_START_TIME); let collection_params = mock_collection_params_1(Some(start_time)); let minter_params = minter_params_token(num_tokens); @@ -35,9 +34,10 @@ pub fn standard_minter_template(num_tokens: u32) -> MinterTemplateResponse MarketplaceV2Template { let mut vt = standard_minter_template(num_tokens); - let fair_burn = setup_fair_burn(&mut vt.router, &vt.accts.creator).unwrap(); let royalty_registry = setup_royalty_registry(&mut vt.router, &vt.accts.creator).unwrap(); + let fee_manager = vt.accts.fee_manager.clone(); + let marketplace = setup_marketplace( &mut vt.router, - fair_burn.clone(), + fee_manager.clone(), royalty_registry.clone(), vt.accts.creator.clone(), ) @@ -70,12 +71,14 @@ pub fn marketplace_v2_template(num_tokens: u32) -> MarketplaceV2Template { let collection = vt.collection_response_vec[0].collection.clone().unwrap(); let minter = vt.collection_response_vec[0].minter.clone().unwrap(); + setup_block_time(&mut vt.router, GENESIS_MINT_START_TIME, None); + MarketplaceV2Template { minter_template: vt, contracts: TestContracts { collection, minter, - fair_burn, + fee_manager, royalty_registry, marketplace, }, diff --git a/contracts/marketplace-v2/src/testing/tests.rs b/contracts/marketplace-v2/src/testing/tests.rs index 43e343e1..bbb4d2de 100644 --- a/contracts/marketplace-v2/src/testing/tests.rs +++ b/contracts/marketplace-v2/src/testing/tests.rs @@ -1,4 +1,6 @@ #[cfg(test)] +mod admin; +#[cfg(test)] mod ask_queries; #[cfg(test)] mod asks; @@ -12,5 +14,3 @@ mod offer_queries; mod offers; #[cfg(test)] mod sales; -#[cfg(test)] -mod sudo; diff --git a/contracts/marketplace-v2/src/testing/tests/admin.rs b/contracts/marketplace-v2/src/testing/tests/admin.rs new file mode 100644 index 00000000..6ffb809a --- /dev/null +++ b/contracts/marketplace-v2/src/testing/tests/admin.rs @@ -0,0 +1,141 @@ +use crate::{ + msg::{ExecuteMsg, QueryMsg}, + state::{AllowDenoms, Config}, + testing::{ + helpers::utils::assert_error, + setup::{ + msg::MarketAccounts, + templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + }, + }, +}; + +use cosmwasm_std::Addr; +use cw_multi_test::Executor; +use sg_marketplace_common::MarketplaceStdError; +use std::vec; +use test_suite::common_setup::msg::MinterTemplateResponse; + +#[test] +fn try_admin_update_config() { + let MarketplaceV2Template { + minter_template: + MinterTemplateResponse { + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder: _, + fee_manager: _, + }, + .. + }, + contracts: + TestContracts { + collection: _, + minter: _, + fee_manager: _, + royalty_registry: _, + marketplace, + }, + } = marketplace_v2_template(10_000); + + let config: Config = router + .wrap() + .query_wasm_smart(&marketplace, &QueryMsg::Config {}) + .unwrap(); + + let delta = 1u64; + let fee_manager = "fee_manager_test".to_string(); + let royalty_registry = "royalty_registry_test".to_string(); + let protocol_fee_bps = config.protocol_fee_bps + delta; + let max_royalty_fee_bps = config.max_royalty_fee_bps + delta; + let maker_reward_bps = config.maker_reward_bps + delta; + let taker_reward_bps = config.taker_reward_bps + delta; + + let update_config_msg = ExecuteMsg::UpdateConfig { + config: Config { + fee_manager: fee_manager.clone(), + royalty_registry: royalty_registry.clone(), + protocol_fee_bps, + max_royalty_fee_bps, + maker_reward_bps, + taker_reward_bps, + }, + }; + + // None admin cannot update config + let response = router.execute_contract(owner, marketplace.clone(), &update_config_msg, &[]); + assert_error( + response, + MarketplaceStdError::Unauthorized( + "only the admin of contract can perform this action".to_string(), + ) + .to_string(), + ); + + let response = router.execute_contract(creator, marketplace.clone(), &update_config_msg, &[]); + assert!(response.is_ok()); + let config: Config = router + .wrap() + .query_wasm_smart(&marketplace, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(config.fee_manager, fee_manager); + assert_eq!(config.royalty_registry, royalty_registry); + assert_eq!(config.protocol_fee_bps, protocol_fee_bps); + assert_eq!(config.max_royalty_fee_bps, max_royalty_fee_bps); + assert_eq!(config.maker_reward_bps, maker_reward_bps); + assert_eq!(config.taker_reward_bps, taker_reward_bps); +} + +#[test] +fn try_admin_update_allow_denoms() { + let MarketplaceV2Template { + minter_template: + MinterTemplateResponse { + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder: _, + fee_manager: _, + }, + .. + }, + contracts: + TestContracts { + collection: _, + minter: _, + fee_manager: _, + royalty_registry: _, + marketplace, + }, + } = marketplace_v2_template(10_000); + + let new_denom_setting = AllowDenoms::Excludes(vec!["ujuno".to_string()]); + let update_allow_denoms = ExecuteMsg::UpdateAllowDenoms { + allow_denoms: new_denom_setting.clone(), + }; + + // None admin cannot update config + let response = router.execute_contract(owner, marketplace.clone(), &update_allow_denoms, &[]); + assert_error( + response, + MarketplaceStdError::Unauthorized( + "only the admin of contract can perform this action".to_string(), + ) + .to_string(), + ); + + let response = router.execute_contract(creator, marketplace.clone(), &update_allow_denoms, &[]); + assert!(response.is_ok()); + let allow_denoms: AllowDenoms = router + .wrap() + .query_wasm_smart(&marketplace, &QueryMsg::AllowDenoms {}) + .unwrap(); + + assert_eq!(allow_denoms, new_denom_setting); +} diff --git a/contracts/marketplace-v2/src/testing/tests/ask_queries.rs b/contracts/marketplace-v2/src/testing/tests/ask_queries.rs index 2c4626c5..8f7c2687 100644 --- a/contracts/marketplace-v2/src/testing/tests/ask_queries.rs +++ b/contracts/marketplace-v2/src/testing/tests/ask_queries.rs @@ -1,13 +1,12 @@ use crate::{ - msg::{AsksByCollectionOffset, AsksByCreatorOffset, AsksByExpirationOffset, AsksByPriceOffset}, - testing::setup::{msg::MarketAccounts, setup_marketplace::JUNO_DENOM}, -}; -use crate::{ - msg::{OrderOptions, QueryMsg}, - state::{Ask, Config, ExpirationInfo, PriceRange}, + msg::{PriceOffset, QueryMsg}, + orders::{Ask, OrderDetails}, testing::{ helpers::marketplace::mint_and_set_ask, - setup::templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + setup::{ + msg::MarketAccounts, + templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + }, }, }; @@ -30,6 +29,7 @@ fn try_query_asks_by_collection() { creator, owner, bidder: _, + fee_manager: _, }, .. }, @@ -37,7 +37,7 @@ fn try_query_asks_by_collection() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -45,28 +45,10 @@ fn try_query_asks_by_collection() { setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - let num_nfts: u8 = 4; for idx in 1..(num_nfts + 1) { let token_id = idx.to_string(); - let ask_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let price = coin(1000000u128 + idx as u128, NATIVE_DENOM); mint_and_set_ask( &mut router, &creator, @@ -75,137 +57,12 @@ fn try_query_asks_by_collection() { &marketplace, &collection, &token_id.to_string(), - &ask_price, - &[config.listing_fee.clone()], - None, - ); - } - - // Other collection address returns no asks - let dummy_collection = Addr::unchecked("dummy_collection"); - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByCollection { - collection: dummy_collection.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(AsksByCollectionOffset { - token_id: "2".to_string(), - })), - max: Some(QueryBound::Exclusive(AsksByCollectionOffset { - token_id: "5".to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(asks.len(), 0); - - // Correct number of asks returned for collection - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByCollection { - collection: collection.to_string(), - query_options: Some(QueryOptions { - descending: None, - limit: None, - min: None, - max: None, - }), - }, - ) - .unwrap(); - assert_eq!(asks.len(), num_nfts as usize); - - // Query Options work - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByCollection { - collection: collection.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(AsksByCollectionOffset { - token_id: "2".to_string(), - })), - max: Some(QueryBound::Exclusive(AsksByCollectionOffset { - token_id: "5".to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(asks.len(), 2); - assert_eq!(asks[0].token_id, "4".to_string()); - assert_eq!(asks[1].token_id, "3".to_string()); -} - -#[test] -fn try_query_asks_by_price() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder: _, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), + &[], + OrderDetails { + price, + asset_recipient: None, + finder: None, }, - ) - .unwrap(); - - let num_nfts: u8 = 4; - for idx in 1..(num_nfts + 1) { - let token_id = idx.to_string(); - let ask_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - &token_id.to_string(), - &ask_price, - &[config.listing_fee.clone()], - None, ); } @@ -215,57 +72,21 @@ fn try_query_asks_by_price() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByPrice { + &QueryMsg::AsksByCollectionDenom { collection: dummy_collection.to_string(), denom: NATIVE_DENOM.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(AsksByPriceOffset { - token_id: "2".to_string(), - amount: 0u128, - })), - max: Some(QueryBound::Exclusive(AsksByPriceOffset { - token_id: "5".to_string(), - amount: 0u128, - })), - }), - }, - ) - .unwrap(); - assert_eq!(asks.len(), 0); - - // Other denoms returns no asks - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByPrice { - collection: collection.to_string(), - denom: JUNO_DENOM.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(AsksByPriceOffset { - token_id: "2".to_string(), - amount: 0u128, - })), - max: Some(QueryBound::Exclusive(AsksByPriceOffset { - token_id: "5".to_string(), - amount: 0u128, - })), - }), + query_options: None, }, ) .unwrap(); assert_eq!(asks.len(), 0); - // Correct number of asks returned for correct collection and denom + // Correct number of asks returned for collection let asks = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByPrice { + &QueryMsg::AsksByCollectionDenom { collection: collection.to_string(), denom: NATIVE_DENOM.to_string(), query_options: None, @@ -279,28 +100,27 @@ fn try_query_asks_by_price() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByPrice { + &QueryMsg::AsksByCollectionDenom { collection: collection.to_string(), denom: NATIVE_DENOM.to_string(), query_options: Some(QueryOptions { descending: Some(true), limit: None, - min: Some(QueryBound::Exclusive(AsksByPriceOffset { - amount: native_denom_price_range.min.u128() + 2, - token_id: "".to_string(), + min: Some(QueryBound::Exclusive(PriceOffset { + id: "".to_string(), + amount: 1000003u128, })), - max: Some(QueryBound::Exclusive(AsksByPriceOffset { - amount: native_denom_price_range.min.u128() + 5, - token_id: "".to_string(), + max: Some(QueryBound::Exclusive(PriceOffset { + id: "".to_string(), + amount: 1000005u128, })), }), }, ) .unwrap(); - assert_eq!(asks.len(), 3); - assert_eq!(asks[0].token_id, "4".to_string()); - assert_eq!(asks[1].token_id, "3".to_string()); - assert_eq!(asks[2].token_id, "2".to_string()); + assert_eq!(asks.len(), 2); + assert_eq!(asks[0].details.price.amount.u128(), 1000004u128); + assert_eq!(asks[1].details.price.amount.u128(), 1000003u128); } #[test] @@ -314,6 +134,7 @@ fn try_query_asks_by_creator() { creator, owner, bidder: _, + fee_manager: _, }, .. }, @@ -321,7 +142,7 @@ fn try_query_asks_by_creator() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -329,28 +150,10 @@ fn try_query_asks_by_creator() { setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - let num_nfts: u8 = 4; for idx in 1..(num_nfts + 1) { let token_id = idx.to_string(); - let ask_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let price = coin(1000000u128 + idx as u128, NATIVE_DENOM); mint_and_set_ask( &mut router, &creator, @@ -359,9 +162,12 @@ fn try_query_asks_by_creator() { &marketplace, &collection, &token_id.to_string(), - &ask_price, - &[config.listing_fee.clone()], - None, + &[], + OrderDetails { + price, + asset_recipient: None, + finder: None, + }, ); } @@ -371,8 +177,9 @@ fn try_query_asks_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByCreator { + &QueryMsg::AsksByCreatorCollection { creator: dummy_creator.to_string(), + collection: collection.to_string(), query_options: None, }, ) @@ -384,8 +191,9 @@ fn try_query_asks_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByCreator { + &QueryMsg::AsksByCreatorCollection { creator: owner.to_string(), + collection: collection.to_string(), query_options: None, }, ) @@ -397,147 +205,18 @@ fn try_query_asks_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::AsksByCreator { + &QueryMsg::AsksByCreatorCollection { creator: owner.to_string(), + collection: collection.to_string(), query_options: Some(QueryOptions { descending: Some(true), limit: Some(2), - min: Some(QueryBound::Inclusive(AsksByCreatorOffset { - collection: "".to_string(), - token_id: "".to_string(), - })), - max: Some(QueryBound::Exclusive(AsksByCreatorOffset { - collection: collection.to_string(), - token_id: "5".to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(asks.len(), 2); - assert_eq!(asks[0].token_id, "4".to_string()); - assert_eq!(asks[1].token_id, "3".to_string()); -} - -#[test] -fn try_query_asks_by_expiration() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder: _, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let block_time = router.block_info().time; - - let num_nfts: u8 = 4; - for idx in 1..(num_nfts + 1) { - let token_id = idx.to_string(); - let ask_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - &token_id.to_string(), - &ask_price, - &[ - config.listing_fee.clone(), - config.min_removal_reward.clone(), - ], - Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(idx as u64), - removal_reward: config.min_removal_reward.clone(), + min: None, + max: None, }), - }), - ); - } - - // Correct number of asks returned for correct creator - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByExpiration { - query_options: None, }, ) .unwrap(); - assert_eq!(asks.len(), num_nfts as usize); - // Query Options work - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByExpiration { - query_options: Some(QueryOptions { - descending: Some(true), - limit: Some(2), - min: Some(QueryBound::Exclusive(AsksByExpirationOffset { - collection: "".to_string(), - token_id: "".to_string(), - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(3u64) - .seconds(), - })), - max: Some(QueryBound::Inclusive(AsksByExpirationOffset { - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(5u64) - .seconds(), - collection: "".to_string(), - token_id: "".to_string(), - })), - }), - }, - ) - .unwrap(); assert_eq!(asks.len(), 2); - assert_eq!(asks[0].token_id, "4".to_string()); - assert_eq!(asks[1].token_id, "3".to_string()); } diff --git a/contracts/marketplace-v2/src/testing/tests/asks.rs b/contracts/marketplace-v2/src/testing/tests/asks.rs index 7b3ce070..21795410 100644 --- a/contracts/marketplace-v2/src/testing/tests/asks.rs +++ b/contracts/marketplace-v2/src/testing/tests/asks.rs @@ -1,6 +1,7 @@ use crate::{ - msg::{ExecuteMsg, OrderOptions, QueryMsg, UpdateVal}, - state::{Ask, Config, ExpirationInfo, KeyString, PriceRange}, + helpers::generate_id, + msg::{ExecuteMsg, QueryMsg}, + orders::{Ask, OrderDetails}, testing::{ helpers::{ marketplace::mint_and_set_ask, @@ -10,27 +11,22 @@ use crate::{ setup::{ msg::MarketAccounts, setup_accounts::setup_additional_account, - setup_marketplace::{ATOM_DENOM, JUNO_DENOM}, + setup_marketplace::JUNO_DENOM, templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, }, }, ContractError, }; +use cosmwasm_std::Addr; use cosmwasm_std::{coin, StdError}; -use cosmwasm_std::{Addr, Uint128}; use cw_multi_test::Executor; -use cw_utils::NativeBalance; use sg_marketplace_common::MarketplaceStdError; -use sg_std::GENESIS_MINT_START_TIME; use sg_std::NATIVE_DENOM; -use std::ops::{Add, Sub}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; +use test_suite::common_setup::msg::MinterTemplateResponse; #[test] -fn try_set_simple_ask() { +fn try_set_ask() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { @@ -40,6 +36,7 @@ fn try_set_simple_ask() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -47,239 +44,95 @@ fn try_set_simple_ask() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let _bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let token_id = "1"; - mint_for(&mut router, &creator, &owner, &minter, token_id); + let num_nfts: u8 = 4; + let mut token_ids: Vec = vec![]; + for idx in 1..(num_nfts + 1) { + let token_id = idx.to_string(); + mint_for(&mut router, &creator, &owner, &minter, &token_id); + token_ids.push(token_id.clone()); + } // Create ask unowned token fails let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(1_000_000, NATIVE_DENOM), - order_options: None, + token_id: token_ids[0].clone(), + details: OrderDetails { + price: coin(1_000_000, NATIVE_DENOM), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - bidder, - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(bidder, marketplace.clone(), &set_ask, &[]); assert_error(response, StdError::generic_err("Unauthorized").to_string()); // Create ask without token approval fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(1_000_000, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_err()); // Create ask with invalid denom fails - approve(&mut router, &owner, &collection, &marketplace, token_id); - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(1_000_000, JUNO_DENOM), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InvalidInput("invalid denom".to_string()).to_string(), - ); - - // Create ask with price too low fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128() - 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InvalidInput("price too low 99999 < 100000".to_string()).to_string(), - ); - - // Create ask with price too high fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.max.u128() + 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InvalidInput("price too high 100000000000001 > 100000000000000".to_string()) - .to_string(), + approve( + &mut router, + &owner, + &collection, + &marketplace, + &token_ids[0], ); - - // Create ask without listing fee fails let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: None, + token_id: token_ids[0].clone(), + details: OrderDetails { + price: coin(1_000_000, JUNO_DENOM), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - config.listing_fee.amount.u128() - 1u128, - &config.listing_fee.denom, - )], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert_error( response, - ContractError::InsufficientFunds { - expected: coin(1000000u128, NATIVE_DENOM), - } - .to_string(), + ContractError::InvalidInput("invalid denom".to_string()).to_string(), ); - // Create simple ask succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); + // Create ask succeeds + let asset_recipient = Addr::unchecked("asset_recipient".to_string()); + let finder = Addr::unchecked("finder".to_string()); + let price = coin(1_000_000, NATIVE_DENOM); let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, + token_id: token_ids[0].clone(), + details: OrderDetails { + price: price.clone(), + asset_recipient: Some(asset_recipient.to_string()), + finder: Some(finder.to_string()), + }, }; - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before - .sub(config.listing_fee.clone()) - .unwrap(), - owner_native_balances_after - ); - + let ask_id = generate_id(vec![collection.as_bytes(), token_ids[0].as_bytes()]); let ask: Ask = router .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) + .query_wasm_smart(&marketplace, &QueryMsg::Ask(ask_id)) .unwrap(); - assert_eq!(ask.order_info.price, price); - assert_eq!(ask.order_info.creator, owner); - assert_eq!(ask.order_info.asset_recipient, None); - assert_eq!(ask.order_info.finders_fee_bps, None); - assert_eq!(ask.order_info.expiration_info, None); + assert_eq!(ask.creator, owner); + assert_eq!(ask.collection, collection); + assert_eq!(ask.token_id, token_ids[0]); + assert_eq!(ask.details.price, price); + assert_eq!(ask.details.asset_recipient, Some(asset_recipient)); + assert_eq!(ask.details.finder, Some(finder)); // Create duplicate ask fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert_error(response, "Generic error: Unauthorized".to_string()); - - // Overpay listing fee succeeds - let token_id = "2"; - mint_for(&mut router, &creator, &owner, &minter, token_id); - approve(&mut router, &owner, &collection, &marketplace, token_id); - - let overpay_fees = vec![ - coin( - config.listing_fee.amount.u128() * 10u128, - &config.listing_fee.denom, - ), - coin(config.listing_fee.amount.u128(), ATOM_DENOM), - ]; - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price, - order_options: None, - }; - let response = - router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &overpay_fees); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before - .sub(config.listing_fee) - .unwrap(), - owner_native_balances_after - ); } #[test] -pub fn try_set_complex_ask() { +pub fn try_update_ask() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { @@ -288,7 +141,8 @@ pub fn try_set_complex_ask() { MarketAccounts { creator, owner, - bidder: _, + bidder, + fee_manager: _, }, .. }, @@ -296,7 +150,7 @@ pub fn try_set_complex_ask() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -305,465 +159,75 @@ pub fn try_set_complex_ask() { let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); let finder = setup_additional_account(&mut router, "finder").unwrap(); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( + let num_nfts: u8 = 4; + let mut token_ids: Vec = vec![]; + for idx in 1..(num_nfts + 1) { + let token_id = idx.to_string(); + mint_and_set_ask( + &mut router, + &creator, + &owner, + &minter, &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), + &collection, + &token_id, + &[], + OrderDetails { + price: coin(1000000 + idx as u128, NATIVE_DENOM), + asset_recipient: Some(asset_recipient.to_string()), + finder: Some(finder.to_string()), }, - ) - .unwrap(); - - let token_id = "1"; - mint_for(&mut router, &creator, &owner, &minter, token_id); - approve(&mut router, &owner, &collection, &marketplace, token_id); - - // Create ask with finder sender fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(owner.to_string()), - finders_fee_bps: None, - expiration_info: None, - }), - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput("finder should not be sender".to_string()).to_string(), - ); - - // Create ask with finders fee above 100% fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(10001), - expiration_info: None, - }), - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput("finders_fee_bps is above 100%".to_string()).to_string(), - ); - - // Create ask with expiration info fails when no fee is paid - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(1000), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), JUNO_DENOM), - }), - }), - }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput(format!( - "removal reward must be at least {}", - config.min_removal_reward - )) - .to_string(), - ); - - // Create ask with expiration too soon fails - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { + ); + token_ids.push(token_id.clone()); + } + + // Non creator updating ask fails + let ask_id = generate_id(vec![ + collection.as_bytes(), + token_ids[0 as usize].as_bytes(), + ]); + let update_ask = ExecuteMsg::UpdateAsk { + id: ask_id.clone(), + details: OrderDetails { + price: coin(1000000, NATIVE_DENOM).clone(), asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(100), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds - 1), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }), - }), + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); + let response = router.execute_contract(bidder.clone(), marketplace.clone(), &update_ask, &[]); assert_error( response, - ContractError::InvalidInput("expiration is below minimum".to_string()).to_string(), - ); - - // Create ask with all valid parameters succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - let finders_fee_bps = 100; - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: Some(asset_recipient.to_string()), - finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; - - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[coin( - (config.listing_fee.amount + config.min_removal_reward.amount).u128(), - &config.listing_fee.denom, - )], - ); - assert!(response.is_ok()); - - let ask: Ask = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) - .unwrap(); - assert_eq!(ask.order_info.price, price); - assert_eq!(ask.order_info.creator, owner); - assert_eq!( - ask.order_info.asset_recipient, - Some(asset_recipient.clone()) - ); - assert_eq!(ask.order_info.finders_fee_bps, Some(finders_fee_bps)); - assert_eq!(ask.order_info.expiration_info, Some(expiration_info)); - - // Create ask with high removal reward succeeds - let token_id = "2"; - mint_for(&mut router, &creator, &owner, &minter, token_id); - approve(&mut router, &owner, &collection, &marketplace, token_id); - - let removal_reward_amount = config.min_removal_reward.amount * Uint128::from(10u8); - let overpay_fees = vec![ - coin( - config.listing_fee.amount.u128() * 10u128 + removal_reward_amount.u128(), - &config.listing_fee.denom, - ), - coin(config.listing_fee.amount.u128(), ATOM_DENOM), - ]; - - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(removal_reward_amount.u128(), NATIVE_DENOM), - }; - - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let set_ask = ExecuteMsg::SetAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: Some(asset_recipient.to_string()), - finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; - let response = - router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &overpay_fees); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before - .sub(config.listing_fee) - .unwrap() - .sub(coin(removal_reward_amount.u128(), NATIVE_DENOM)) - .unwrap(), - owner_native_balances_after - ); - - let ask: Ask = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) - .unwrap(); - assert_eq!(ask.order_info.price, price); - assert_eq!(ask.order_info.creator, owner); - assert_eq!(ask.order_info.asset_recipient, Some(asset_recipient)); - assert_eq!(ask.order_info.finders_fee_bps, Some(finders_fee_bps)); - assert_eq!(ask.order_info.expiration_info, Some(expiration_info)); -} - -#[test] -pub fn try_update_ask() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder: _, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); - let asset_recipient2 = setup_additional_account(&mut router, "asset_recipient2").unwrap(); - let finder = setup_additional_account(&mut router, "finder").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, + MarketplaceStdError::Unauthorized( + "only the creator of ask can perform this action".to_string(), ) - .unwrap(); - - let token_id = "1"; - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - token_id, - &coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - Some(OrderOptions { - asset_recipient: Some(asset_recipient.to_string()), - finder: Some(finder.to_string()), - finders_fee_bps: Some(100), - expiration_info: None, - }), + .to_string(), ); - // Setting asset_recipient and finders_fee_bps succeeds + // Setting asset_recipient and finder to None succeeds + let new_price = coin(1000001u128, NATIVE_DENOM); + let ask_id = generate_id(vec![ + collection.as_bytes(), + token_ids[0 as usize].as_bytes(), + ]); let update_ask = ExecuteMsg::UpdateAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: Some(UpdateVal::Set(asset_recipient2.to_string())), - finders_fee_bps: Some(UpdateVal::Set(200)), - expiration_info: None, + id: ask_id.clone(), + details: OrderDetails { + price: new_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract(owner.clone(), marketplace.clone(), &update_ask, &[]); assert!(response.is_ok()); let ask: Ask = router .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) + .query_wasm_smart(&marketplace, &QueryMsg::Ask(ask_id)) .unwrap(); - assert_eq!(ask.order_info.asset_recipient, Some(asset_recipient2)); - assert_eq!(ask.order_info.finders_fee_bps, Some(200)); - - // Setting expiration_info without fee fails - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; - let update_ask = ExecuteMsg::UpdateAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), - }; - - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &update_ask, - &[coin(config.min_removal_reward.amount.u128(), ATOM_DENOM)], - ); - assert_error( - response, - ContractError::InsufficientFunds { - expected: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - } - .to_string(), - ); - - // Setting expiration_info with fee succeeds - let update_ask = ExecuteMsg::UpdateAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info)), - }; - - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &update_ask, - &[config.min_removal_reward.clone()], - ); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - - assert_eq!( - owner_native_balances_before - .sub(config.min_removal_reward.clone()) - .unwrap(), - owner_native_balances_after - ); - - // Updating expiration_info refunds previous paid removal reward - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin( - config.min_removal_reward.amount.u128() * 2u128, - NATIVE_DENOM, - ), - }; - let update_ask = ExecuteMsg::UpdateAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), - }; - - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &update_ask, - &[expiration_info.removal_reward.clone()], - ); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - - assert_eq!( - owner_native_balances_before - .sub(config.min_removal_reward) - .unwrap(), - owner_native_balances_after - ); - - // Removing expiration_info refunds removal reward - let update_ask = ExecuteMsg::UpdateAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Unset), - }; - - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &update_ask, - &[expiration_info.removal_reward.clone()], - ); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before.add(expiration_info.removal_reward), - owner_native_balances_after - ); + assert_eq!(ask.details.price, new_price); + assert_eq!(ask.details.asset_recipient, None); + assert_eq!(ask.details.finder, None); } #[test] @@ -777,6 +241,7 @@ pub fn try_remove_ask() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -784,143 +249,56 @@ pub fn try_remove_ask() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( + let num_nfts: u8 = 4; + let mut token_ids: Vec = vec![]; + for idx in 1..(num_nfts + 1) { + let token_id = idx.to_string(); + mint_and_set_ask( + &mut router, + &creator, + &owner, + &minter, &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), + &collection, + &token_id, + &[], + OrderDetails { + price: coin(1000000 + idx as u128, NATIVE_DENOM), + asset_recipient: None, + finder: None, }, - ) - .unwrap(); - - let token_id = "1"; - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - token_id, - &coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - &[config.listing_fee.clone()], - None, - ); + ); + token_ids.push(token_id.clone()); + } // Removing ask as non creator fails - let remove_ask = ExecuteMsg::RemoveAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - }; + let ask_id = generate_id(vec![ + collection.as_bytes(), + token_ids[0 as usize].as_bytes(), + ]); + let remove_ask = ExecuteMsg::RemoveAsk { id: ask_id.clone() }; let response = router.execute_contract(bidder.clone(), marketplace.clone(), &remove_ask, &[]); assert_error( response, MarketplaceStdError::Unauthorized( - "only the creator of order can perform this action".to_string(), + "only the creator of ask can perform this action".to_string(), ) .to_string(), ); // Removing ask as creator succeeds - let remove_ask = ExecuteMsg::RemoveAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - }; - let response = router.execute_contract(owner.clone(), marketplace.clone(), &remove_ask, &[]); - assert!(response.is_ok()); - - let ask = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) - .unwrap(); - assert!(ask.is_none()); - - let token_id = "2"; - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - token_id, - &coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - &[coin( - config.listing_fee.amount.u128() + config.min_removal_reward.amount.u128(), - NATIVE_DENOM, - )], - Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: config.min_removal_reward, - }), - }), - ); - - // Cannot remove ask that is not expired - let remove_ask = ExecuteMsg::RemoveExpiredAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - }; let response = router.execute_contract(owner.clone(), marketplace.clone(), &remove_ask, &[]); - - assert_error( - response, - ContractError::EntityNotExpired(format!( - "ask {}", - Ask::build_key(&collection, &token_id.to_string()).to_string() - )) - .to_string(), - ); - - // Anyone can remove ask that is expired - setup_block_time( - &mut router, - block_time - .plus_seconds(config.min_expiration_seconds) - .nanos(), - None, - ); - let remove_ask = ExecuteMsg::RemoveExpiredAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - }; - let response = router.execute_contract(bidder, marketplace.clone(), &remove_ask, &[]); assert!(response.is_ok()); let ask = router .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Ask { - collection: collection.to_string(), - token_id: token_id.to_string(), - }, - ) + .query_wasm_smart::>(&marketplace, &QueryMsg::Ask(ask_id)) .unwrap(); assert!(ask.is_none()); } diff --git a/contracts/marketplace-v2/src/testing/tests/collection_offer_queries.rs b/contracts/marketplace-v2/src/testing/tests/collection_offer_queries.rs index 2ebabb56..c942b47e 100644 --- a/contracts/marketplace-v2/src/testing/tests/collection_offer_queries.rs +++ b/contracts/marketplace-v2/src/testing/tests/collection_offer_queries.rs @@ -1,15 +1,14 @@ use crate::{ - msg::{ - CollectionOffersByCollectionOffset, CollectionOffersByCreatorOffset, - CollectionOffersByExpirationOffset, CollectionOffersByPriceOffset, ExecuteMsg, - OrderOptions, QueryMsg, - }, - state::{CollectionOffer, Config, ExpirationInfo, PriceRange}, - testing::setup::{ - msg::MarketAccounts, - setup_accounts::setup_additional_account, - setup_marketplace::ATOM_DENOM, - templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + msg::{ExecuteMsg, PriceOffset, QueryMsg}, + orders::{CollectionOffer, OrderDetails}, + testing::{ + helpers::utils::find_attrs, + setup::{ + msg::MarketAccounts, + setup_accounts::setup_additional_account, + setup_marketplace::ATOM_DENOM, + templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + }, }, }; @@ -17,10 +16,8 @@ use cosmwasm_std::coin; use cosmwasm_std::Addr; use cw_multi_test::Executor; use sg_index_query::{QueryBound, QueryOptions}; -use sg_std::{GENESIS_MINT_START_TIME, NATIVE_DENOM}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; +use sg_std::NATIVE_DENOM; +use test_suite::common_setup::msg::MinterTemplateResponse; #[test] fn try_query_collection_offers_by_collection() { @@ -39,31 +36,20 @@ fn try_query_collection_offers_by_collection() { }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { + let num_collection_offers: u8 = 4; + let mut collection_offer_ids: Vec = vec![]; + for idx in 1..(num_collection_offers + 1) { let collection_bidder = setup_additional_account(&mut router, &format!("collection-bidder-{}", idx)).unwrap(); - let collection_offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + + let collection_offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: None, + details: OrderDetails { + price: collection_offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( collection_bidder.clone(), @@ -72,64 +58,24 @@ fn try_query_collection_offers_by_collection() { &[collection_offer_price.clone()], ); assert!(response.is_ok()); + let offer_id = find_attrs(response.unwrap(), "wasm-set-collection-offer", "id") + .pop() + .unwrap(); + collection_offer_ids.push(offer_id); } - // Other collection address returns no collection offers - let dummy_collection = Addr::unchecked("dummy_collection"); let collection_offers = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffersByCollection { - collection: dummy_collection.to_string(), - query_options: None, - }, + &QueryMsg::CollectionOffers(collection_offer_ids.clone()), ) .unwrap(); - assert_eq!(collection_offers.len(), 0); + assert_eq!(collection_offers.len(), num_collection_offers as usize); - // Correct number of offers returned for collection - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), num_orders as usize); - - // Query Options work - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByCollection { - collection: collection.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(CollectionOffersByCollectionOffset { - creator: "collection-bidder-2".to_string(), - })), - max: Some(QueryBound::Inclusive(CollectionOffersByCollectionOffset { - creator: "collection-bidder-4".to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), 2); - assert_eq!( - collection_offers[0].order_info.creator, - "collection-bidder-4".to_string() - ); - assert_eq!( - collection_offers[1].order_info.creator, - "collection-bidder-3".to_string() - ); + for (idx, offer) in collection_offers.iter().enumerate() { + assert_eq!(offer.id, collection_offer_ids[idx]); + } } #[test] @@ -149,31 +95,18 @@ fn try_query_collection_offers_by_token_price() { }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { + let num_collection_offers: u8 = 4; + for idx in 1..(num_collection_offers + 1) { let collection_bidder = setup_additional_account(&mut router, &format!("collection-bidder-{}", idx)).unwrap(); - let collection_offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let collection_offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: None, + details: OrderDetails { + price: collection_offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( collection_bidder.clone(), @@ -225,10 +158,10 @@ fn try_query_collection_offers_by_token_price() { }, ) .unwrap(); - assert_eq!(collection_offers.len(), num_orders as usize); + assert_eq!(collection_offers.len(), num_collection_offers as usize); // Query Options work - let collection_offers = router + let qo_collection_offers = router .wrap() .query_wasm_smart::>( &marketplace, @@ -238,31 +171,29 @@ fn try_query_collection_offers_by_token_price() { query_options: Some(QueryOptions { descending: Some(true), limit: None, - min: Some(QueryBound::Exclusive(CollectionOffersByPriceOffset { - amount: native_denom_price_range.min.u128() + 2, - creator: "".to_string(), + min: Some(QueryBound::Exclusive(PriceOffset { + id: collection_offers[0].id.clone(), + amount: collection_offers[0].details.price.amount.u128(), })), - max: Some(QueryBound::Exclusive(CollectionOffersByPriceOffset { - amount: native_denom_price_range.min.u128() + 5, - creator: "".to_string(), + max: Some(QueryBound::Exclusive(PriceOffset { + id: collection_offers[3].id.clone(), + amount: collection_offers[3].details.price.amount.u128(), })), }), }, ) .unwrap(); - assert_eq!(collection_offers.len(), 3); - assert_eq!( - collection_offers[0].order_info.price, - coin(native_denom_price_range.min.u128() + 4, NATIVE_DENOM) - ); - assert_eq!( - collection_offers[1].order_info.price, - coin(native_denom_price_range.min.u128() + 3, NATIVE_DENOM) - ); - assert_eq!( - collection_offers[2].order_info.price, - coin(native_denom_price_range.min.u128() + 2, NATIVE_DENOM) - ); + + assert_eq!(qo_collection_offers.len(), 2); + + for (idx, offer) in qo_collection_offers.iter().enumerate() { + let offer_idx = 2 - idx; + assert_eq!(offer.id, collection_offers[offer_idx].id); + assert_eq!( + offer.details.price.amount.u128(), + collection_offers[offer_idx].details.price.amount.u128() + ); + } } #[test] @@ -271,7 +202,7 @@ fn try_query_collection_offers_by_creator() { minter_template: MinterTemplateResponse { mut router, - accts: MarketAccounts { .. }, + accts: MarketAccounts { bidder, .. }, .. }, contracts: @@ -282,34 +213,19 @@ fn try_query_collection_offers_by_creator() { }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { - let collection_bidder = - setup_additional_account(&mut router, &format!("collection-bidder-{}", idx)).unwrap(); - let collection_offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let num_collection_offers: u8 = 4; + for idx in 1..(num_collection_offers + 1) { + let collection_offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: None, + details: OrderDetails { + price: collection_offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( - collection_bidder.clone(), + bidder.clone(), marketplace.clone(), &set_collection_offer, &[collection_offer_price.clone()], @@ -323,8 +239,9 @@ fn try_query_collection_offers_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffersByCreator { + &QueryMsg::CollectionOffersByCreatorCollection { creator: dummy_creator.to_string(), + collection: collection.to_string(), query_options: None, }, ) @@ -336,161 +253,36 @@ fn try_query_collection_offers_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffersByCreator { - creator: "collection-bidder-1".to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), 1_usize); - - // Query Options work - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByCreator { - creator: "collection-bidder-2".to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: Some(2), - min: Some(QueryBound::Inclusive(CollectionOffersByCreatorOffset { - collection: "".to_string(), - })), - max: Some(QueryBound::Inclusive(CollectionOffersByCreatorOffset { - collection: collection.to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), 1); - assert_eq!( - collection_offers[0].order_info.creator, - Addr::unchecked("collection-bidder-2".to_string()) - ); - assert_eq!(collection_offers[0].collection, collection); -} - -#[test] -fn try_query_collection_offers_by_expiration() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: MarketAccounts { .. }, - .. - }, - contracts: - TestContracts { - collection, - marketplace, - .. - }, - } = marketplace_v2_template(10_000); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { - let collection_bidder = - setup_additional_account(&mut router, &format!("collection-bidder-{}", idx)).unwrap(); - let collection_offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(idx as u64), - removal_reward: config.min_removal_reward.clone(), - }), - }), - }; - let response = router.execute_contract( - collection_bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[ - collection_offer_price.clone(), - config.min_removal_reward.clone(), - ], - ); - assert!(response.is_ok()); - } - - // Correct number of collection offers returned for correct query - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByExpiration { + &QueryMsg::CollectionOffersByCreatorCollection { + creator: bidder.to_string(), + collection: collection.to_string(), query_options: None, }, ) .unwrap(); - assert_eq!(collection_offers.len(), num_orders as usize); + assert_eq!(collection_offers.len(), num_collection_offers as usize); // Query Options work - let collection_offers = router + let qo_collection_offers = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffersByExpiration { + &QueryMsg::CollectionOffersByCreatorCollection { + creator: bidder.to_string(), + collection: collection.to_string(), query_options: Some(QueryOptions { descending: Some(true), limit: Some(2), - min: Some(QueryBound::Exclusive(CollectionOffersByExpirationOffset { - collection: "".to_string(), - creator: "".to_string(), - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(3u64) - .seconds(), - })), - max: Some(QueryBound::Exclusive(CollectionOffersByExpirationOffset { - collection: "".to_string(), - creator: "".to_string(), - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(5u64) - .seconds(), - })), + min: Some(QueryBound::Exclusive(collection_offers[0].id.clone())), + max: Some(QueryBound::Exclusive(collection_offers[3].id.clone())), }), }, ) .unwrap(); - - assert_eq!(collection_offers.len(), 2); - assert_eq!( - collection_offers[0].order_info.creator, - Addr::unchecked("collection-bidder-4") - ); + assert_eq!(qo_collection_offers.len(), 2); assert_eq!( - collection_offers[1].order_info.creator, - Addr::unchecked("collection-bidder-3") + qo_collection_offers[0].creator, + Addr::unchecked(bidder.to_string()) ); + assert_eq!(qo_collection_offers[0].collection, collection); } diff --git a/contracts/marketplace-v2/src/testing/tests/collection_offers.rs b/contracts/marketplace-v2/src/testing/tests/collection_offers.rs index 8670662f..5b8a0b78 100644 --- a/contracts/marketplace-v2/src/testing/tests/collection_offers.rs +++ b/contracts/marketplace-v2/src/testing/tests/collection_offers.rs @@ -1,12 +1,12 @@ use crate::{ - msg::{ExecuteMsg, OrderOptions, QueryMsg, UpdateVal}, - state::{CollectionOffer, Config, ExpirationInfo, KeyString, PriceRange}, + msg::{ExecuteMsg, QueryMsg}, + orders::{CollectionOffer, OrderDetails}, testing::{ - helpers::{nft_functions::mint_for, utils::assert_error}, + helpers::utils::{assert_error, find_attrs}, setup::{ msg::MarketAccounts, setup_accounts::setup_additional_account, - setup_marketplace::{ATOM_DENOM, JUNO_DENOM}, + setup_marketplace::JUNO_DENOM, templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, }, }, @@ -14,225 +14,16 @@ use crate::{ }; use cosmwasm_std::coin; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw_multi_test::Executor; use cw_utils::NativeBalance; -use sg_std::GENESIS_MINT_START_TIME; +use sg_marketplace_common::MarketplaceStdError; use sg_std::NATIVE_DENOM; use std::ops::{Add, Sub}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; - -#[test] -fn try_set_simple_collection_offer() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - let _bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let token_id = "1"; - mint_for(&mut router, &creator, &owner, &minter, token_id); - - // Create collection offer without sufficient offer funds fails - let collection_offer_price = coin(2_000_000, NATIVE_DENOM); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InsufficientFunds { - expected: collection_offer_price, - } - .to_string(), - ); - - // Create collection offer with invalid denom fails - let collection_offer_price = coin(2_000_000, JUNO_DENOM); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[collection_offer_price], - ); - assert_error( - response, - ContractError::InvalidInput("invalid denom".to_string()).to_string(), - ); - - // Create collection offer with price too low fails - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: coin(native_denom_price_range.min.u128() - 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InvalidInput("price too low 99999 < 100000".to_string()).to_string(), - ); - - // Create ask with price too high fails - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: coin(native_denom_price_range.max.u128() + 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[config.listing_fee], - ); - assert_error( - response, - ContractError::InvalidInput("price too high 100000000000001 > 100000000000000".to_string()) - .to_string(), - ); - - // Create simple offer succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: None, - }; - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[price.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - assert_eq!( - bidder_native_balances_before.sub(price.clone()).unwrap(), - bidder_native_balances_after - ); - - let collection_offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!(collection_offer.order_info.price, price); - assert_eq!(collection_offer.order_info.creator, bidder); - assert_eq!(collection_offer.order_info.asset_recipient, None); - assert_eq!(collection_offer.order_info.finders_fee_bps, None); - assert_eq!(collection_offer.order_info.expiration_info, None); - - // Create duplicate offer fails - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[price.clone()], - ); - assert_error( - response, - ContractError::EntityExists(format!( - "collection_offer {}", - CollectionOffer::build_key(&collection, &bidder).to_string() - )) - .to_string(), - ); - - // Overpay listing fee succeeds - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace, - &set_collection_offer, - &[coin(price.amount.u128() * 2u128, NATIVE_DENOM)], - ); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before.sub(price).unwrap(), - owner_native_balances_after - ); -} +use test_suite::common_setup::msg::MinterTemplateResponse; #[test] -pub fn try_set_complex_collection_offer() { +fn try_set_collection_offer() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { @@ -242,6 +33,7 @@ pub fn try_set_complex_collection_offer() { creator: _, owner: _, bidder, + fee_manager: _, }, .. }, @@ -249,242 +41,79 @@ pub fn try_set_complex_collection_offer() { TestContracts { collection, minter: _, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); - let finder = setup_additional_account(&mut router, "finder").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - // Create collection offer with finder sender fails + // Create offer without sufficient offer funds fails + let collection_offer_price = coin(1_000_000, NATIVE_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { + details: OrderDetails { + price: collection_offer_price.clone(), asset_recipient: None, - finder: Some(bidder.to_string()), - finders_fee_bps: None, - expiration_info: None, - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[coin(native_denom_price_range.min.u128(), NATIVE_DENOM)], - ); - assert_error( - response, - ContractError::InvalidInput("finder should not be sender".to_string()).to_string(), - ); - - // Create collection offer with finders fee above 100% fails - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(10001), - expiration_info: None, - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput("finders_fee_bps is above 100%".to_string()).to_string(), - ); - - // Create collection offer with expiration info fails when no fee is paid - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(1000), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), JUNO_DENOM), - }), - }), + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_collection_offer, &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, + collection_offer_price.amount.u128() - 1u128, + NATIVE_DENOM, )], ); - assert_error( - response, - ContractError::InvalidInput(format!( - "removal reward must be at least {}", - config.min_removal_reward - )) - .to_string(), - ); + assert_error(response, ContractError::InsufficientFunds.to_string()); - // Create collection offer with expiration too soon fails + // Create collection_offer with invalid denom fails + let collection_offer_price = coin(1_000_000, JUNO_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { + details: OrderDetails { + price: collection_offer_price.clone(), asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(1000), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds - 1), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }), - }), + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_collection_offer, &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, + collection_offer_price.amount.u128() - 1u128, + JUNO_DENOM, )], ); assert_error( response, - ContractError::InvalidInput("expiration is below minimum".to_string()).to_string(), + ContractError::InvalidInput("invalid denom".to_string()).to_string(), ); - // Create collection offer with all valid parameters succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - let finders_fee_bps = 1000; - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; + // Create collection_offer succeeds, even when overpaid + let asset_recipient = Addr::unchecked("asset_recipient".to_string()); + let finder = Addr::unchecked("finder".to_string()); + let collection_offer_price = coin(1_000_000, NATIVE_DENOM); let set_collection_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { + details: OrderDetails { + price: collection_offer_price.clone(), asset_recipient: Some(asset_recipient.to_string()), finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; - - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_collection_offer, - &[coin( - (config.listing_fee.amount + config.min_removal_reward.amount).u128(), - &config.listing_fee.denom, - )], - ); - assert!(response.is_ok()); - - let collection_offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!(collection_offer.order_info.price, price); - assert_eq!(collection_offer.order_info.creator, bidder); - assert_eq!( - collection_offer.order_info.asset_recipient, - Some(asset_recipient.clone()) - ); - assert_eq!( - collection_offer.order_info.finders_fee_bps, - Some(finders_fee_bps) - ); - assert_eq!( - collection_offer.order_info.expiration_info, - Some(expiration_info) - ); - - // Create collection offer with high removal reward succeeds - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::RemoveCollectionOffer { - collection: collection.to_string(), - }, - &[], - ) - .unwrap(); - - let removal_reward_amount = config.min_removal_reward.amount * Uint128::from(10u8); - let overpay_fees = vec![ - price.clone(), - coin( - removal_reward_amount.u128(), - &config.min_removal_reward.denom, - ), - coin(config.listing_fee.amount.u128(), ATOM_DENOM), - ]; - - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin( - removal_reward_amount.u128(), - &config.min_removal_reward.denom, - ), + }, }; - let bidder_native_balances_before = NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let set_collection_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: Some(asset_recipient.to_string()), - finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_collection_offer, - &overpay_fees, + &[coin( + collection_offer_price.amount.u128() * 2u128, + NATIVE_DENOM, + )], ); assert!(response.is_ok()); @@ -492,38 +121,33 @@ pub fn try_set_complex_collection_offer() { NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); assert_eq!( bidder_native_balances_before - .sub(price.clone()) - .unwrap() - .sub(coin(removal_reward_amount.u128(), NATIVE_DENOM)) + .sub(collection_offer_price.clone()) .unwrap(), bidder_native_balances_after ); + let collection_offer_id = find_attrs(response.unwrap(), "wasm-set-collection-offer", "id") + .pop() + .unwrap(); + let collection_offer = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, + &QueryMsg::CollectionOffer(collection_offer_id.clone()), ) .unwrap() .unwrap(); - assert_eq!(collection_offer.order_info.price, price); - assert_eq!(collection_offer.order_info.creator, bidder); + + assert_eq!(collection_offer.id, collection_offer_id); + assert_eq!(collection_offer.creator, bidder); + assert_eq!(collection_offer.collection, collection); + assert_eq!(collection_offer.details.price, collection_offer_price); assert_eq!( - collection_offer.order_info.asset_recipient, + collection_offer.details.asset_recipient, Some(asset_recipient) ); - assert_eq!( - collection_offer.order_info.finders_fee_bps, - Some(finders_fee_bps) - ); - assert_eq!( - collection_offer.order_info.expiration_info, - Some(expiration_info) - ); + assert_eq!(collection_offer.details.finder, Some(finder)); } #[test] @@ -535,8 +159,9 @@ pub fn try_update_collection_offer() { accts: MarketAccounts { creator: _, - owner: _, + owner, bidder, + fee_manager: _, }, .. }, @@ -544,145 +169,73 @@ pub fn try_update_collection_offer() { TestContracts { collection, minter: _, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let asset_recipient2 = setup_additional_account(&mut router, "asset_recipient2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); + let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); + let finder = setup_additional_account(&mut router, "finder").unwrap(); - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), + let num_collection_offers: u8 = 4; + let mut collection_offer_ids: Vec = vec![]; + for idx in 1..(num_collection_offers + 1) { + let collection_offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); + let set_collection_offer = ExecuteMsg::SetCollectionOffer { + collection: collection.to_string(), + details: OrderDetails { + price: collection_offer_price.clone(), + asset_recipient: None, + finder: None, }, - ) - .unwrap(); - - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - router - .execute_contract( + }; + let response = router.execute_contract( bidder.clone(), marketplace.clone(), - &ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: None, - }, - &[price], - ) - .unwrap(); - - // Setting asset_recipient and finders_fee_bps succeeds + &set_collection_offer, + &[collection_offer_price], + ); + assert!(response.is_ok()); + + let collection_offer_id = find_attrs(response.unwrap(), "wasm-set-collection-offer", "id") + .pop() + .unwrap(); + collection_offer_ids.push(collection_offer_id); + } + + // Non creator updating collection_offer fails let update_collection_offer = ExecuteMsg::UpdateCollectionOffer { - collection: collection.to_string(), - asset_recipient: Some(UpdateVal::Set(asset_recipient2.to_string())), - finders_fee_bps: Some(UpdateVal::Set(2000)), - expiration_info: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &update_collection_offer, - &[], - ); - assert!(response.is_ok()); - - let collection_offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!( - collection_offer.order_info.asset_recipient, - Some(asset_recipient2) - ); - assert_eq!(collection_offer.order_info.finders_fee_bps, Some(2000)); - - // Setting expiration_info without fee fails - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; - let update_collection_offer = ExecuteMsg::UpdateCollectionOffer { - collection: collection.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), + id: collection_offer_ids[0].clone(), + details: OrderDetails { + price: coin(1000000u128, NATIVE_DENOM), + asset_recipient: Some(asset_recipient.to_string()), + finder: Some(finder.to_string()), + }, }; - let response = router.execute_contract( - bidder.clone(), + owner.clone(), marketplace.clone(), &update_collection_offer, &[], ); assert_error( response, - ContractError::InsufficientFunds { - expected: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - } + MarketplaceStdError::Unauthorized( + "only the creator of collection offer can perform this action".to_string(), + ) .to_string(), ); - // Setting expiration_info with fee succeeds + // Updating collection_offer succeeds, wallet is refunded + let new_price = coin(1000000u128, NATIVE_DENOM); let update_collection_offer = ExecuteMsg::UpdateCollectionOffer { - collection: collection.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info)), - }; - - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &update_collection_offer, - &[config.min_removal_reward.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - - assert_eq!( - bidder_native_balances_before - .sub(config.min_removal_reward.clone()) - .unwrap(), - bidder_native_balances_after - ); - - // Updating expiration_info refunds previous paid removal reward - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin( - config.min_removal_reward.amount.u128() * 2u128, - NATIVE_DENOM, - ), - }; - let update_collection_offer = ExecuteMsg::UpdateCollectionOffer { - collection: collection.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), + id: collection_offer_ids[0].clone(), + details: OrderDetails { + price: new_price.clone(), + asset_recipient: None, + finder: None, + }, }; let bidder_native_balances_before = @@ -691,49 +244,20 @@ pub fn try_update_collection_offer() { bidder.clone(), marketplace.clone(), &update_collection_offer, - &[expiration_info.removal_reward.clone()], + &[new_price], ); assert!(response.is_ok()); - let bidder_native_balances_after = NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); assert_eq!( - bidder_native_balances_before - .sub(config.min_removal_reward) - .unwrap(), - bidder_native_balances_after - ); - - // Removing expiration_info refunds removal reward - let update_collection_offer = ExecuteMsg::UpdateCollectionOffer { - collection: collection.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Unset), - }; - - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace, - &update_collection_offer, - &[expiration_info.removal_reward.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder).unwrap()); - - assert_eq!( - bidder_native_balances_before.add(expiration_info.removal_reward), + bidder_native_balances_before.add(coin(1u128, NATIVE_DENOM).clone()), bidder_native_balances_after ); } #[test] -pub fn try_remove_collection_offer() { +pub fn try_remove_offer() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { @@ -741,8 +265,9 @@ pub fn try_remove_collection_offer() { accts: MarketAccounts { creator: _, - owner: _, + owner, bidder, + fee_manager: _, }, .. }, @@ -750,128 +275,54 @@ pub fn try_remove_collection_offer() { TestContracts { collection, minter: _, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: price.clone(), - order_options: None, - }, - &[price.clone()], - ) - .unwrap(); - - // Removing collection offer as creator succeeds - let remove_collection_offer = ExecuteMsg::RemoveCollectionOffer { - collection: collection.to_string(), - }; + let price = coin(1000000u128, NATIVE_DENOM); let response = router.execute_contract( bidder.clone(), marketplace.clone(), - &remove_collection_offer, - &[], - ); - assert!(response.is_ok()); - - let collection_offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap(); - assert!(collection_offer.is_none()); - - // Cannot remove collection offer that is not expired - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), + &ExecuteMsg::SetCollectionOffer { + collection: collection.to_string(), + details: OrderDetails { price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }), - }), + asset_recipient: None, + finder: None, }, - &[price, config.min_removal_reward.clone()], - ) + }, + &[price], + ); + + let collection_offer_id = find_attrs(response.unwrap(), "wasm-set-collection-offer", "id") + .pop() .unwrap(); - let remove_expired_collection_offer = ExecuteMsg::RemoveExpiredCollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), + // Removing collection_offer as non creator fails + let remove_collection_offer = ExecuteMsg::RemoveCollectionOffer { + id: collection_offer_id.clone(), }; let response = router.execute_contract( - bidder2, + owner.clone(), marketplace.clone(), - &remove_expired_collection_offer, + &remove_collection_offer, &[], ); - assert_error( response, - ContractError::EntityNotExpired(format!( - "collection_offer {}", - CollectionOffer::build_key(&collection, &bidder).to_string() - )) + MarketplaceStdError::Unauthorized( + "only the creator of collection offer can perform this action".to_string(), + ) .to_string(), ); - // Anyone can remove collection offer that is expired - setup_block_time( - &mut router, - block_time - .plus_seconds(config.min_expiration_seconds) - .nanos(), - None, - ); - let remove_expired_collection_offer = ExecuteMsg::RemoveExpiredCollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }; + // Removing collection_offer as creator succeeds let response = router.execute_contract( bidder.clone(), marketplace.clone(), - &remove_expired_collection_offer, + &remove_collection_offer, &[], ); assert!(response.is_ok()); @@ -880,10 +331,7 @@ pub fn try_remove_collection_offer() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::CollectionOffer { - collection: collection.to_string(), - creator: bidder.to_string(), - }, + &QueryMsg::CollectionOffer(collection_offer_id), ) .unwrap(); assert!(collection_offer.is_none()); diff --git a/contracts/marketplace-v2/src/testing/tests/offer_queries.rs b/contracts/marketplace-v2/src/testing/tests/offer_queries.rs index f30a6859..b3b5893c 100644 --- a/contracts/marketplace-v2/src/testing/tests/offer_queries.rs +++ b/contracts/marketplace-v2/src/testing/tests/offer_queries.rs @@ -1,14 +1,13 @@ use crate::{ - msg::{ - ExecuteMsg, OffersByCollectionOffset, OffersByCreatorOffset, OffersByExpirationOffset, - OffersByTokenPriceOffset, OrderOptions, QueryMsg, - }, - state::{Config, ExpirationInfo, Offer, PriceRange}, - testing::setup::{ - msg::MarketAccounts, - setup_accounts::setup_additional_account, - setup_marketplace::ATOM_DENOM, - templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + msg::{ExecuteMsg, PriceOffset, QueryMsg}, + orders::{Offer, OrderDetails}, + testing::{ + helpers::utils::find_attrs, + setup::{ + msg::MarketAccounts, + setup_marketplace::JUNO_DENOM, + templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, + }, }, }; @@ -16,115 +15,71 @@ use cosmwasm_std::coin; use cosmwasm_std::Addr; use cw_multi_test::Executor; use sg_index_query::{QueryBound, QueryOptions}; -use sg_std::{GENESIS_MINT_START_TIME, NATIVE_DENOM}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; +use sg_std::NATIVE_DENOM; +use test_suite::common_setup::msg::MinterTemplateResponse; #[test] -fn try_query_offers_by_collection() { +fn try_query_offers() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { mut router, - accts: MarketAccounts { bidder, .. }, + accts: + MarketAccounts { + creator: _, + owner: _, + bidder, + fee_manager: _, + }, .. }, contracts: TestContracts { collection, + minter: _, + fee_manager: _, + royalty_registry: _, marketplace, - .. }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { - let token_id = idx.to_string(); - let offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let num_offers: u8 = 4; + let token_id = "1".to_string(); + let mut offer_ids: Vec = vec![]; + for idx in 1..(num_offers + 1) { + let offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_offer, - &[offer_price.clone()], + &[offer_price], ); assert!(response.is_ok()); - } - // Other collection address returns no offers - let dummy_collection = Addr::unchecked("dummy_collection"); - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByCollection { - collection: dummy_collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(offers.len(), 0); + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); + offer_ids.push(offer_id); + } - // Correct number of offers returned for collection let offers = router .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) + .query_wasm_smart::>(&marketplace, &QueryMsg::Offers(offer_ids.clone())) .unwrap(); - assert_eq!(offers.len(), num_orders as usize); + assert_eq!(offers.len(), num_offers as usize); - // Query Options work - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByCollection { - collection: collection.to_string(), - query_options: Some(QueryOptions { - descending: Some(true), - limit: None, - min: Some(QueryBound::Exclusive(OffersByCollectionOffset { - token_id: "3".to_string(), - creator: "".to_string(), - })), - max: Some(QueryBound::Exclusive(OffersByCollectionOffset { - token_id: "5".to_string(), - creator: "".to_string(), - })), - }), - }, - ) - .unwrap(); - assert_eq!(offers.len(), 2); - assert_eq!(offers[0].token_id, "4".to_string()); - assert_eq!(offers[1].token_id, "3".to_string()); + for (idx, offer) in offers.iter().enumerate() { + assert_eq!(offer.id, offer_ids[idx]); + } } #[test] @@ -133,56 +88,54 @@ fn try_query_offers_by_token_price() { minter_template: MinterTemplateResponse { mut router, - accts: MarketAccounts { .. }, + accts: + MarketAccounts { + creator: _, + owner: _, + bidder, + fee_manager: _, + }, .. }, contracts: TestContracts { collection, + minter: _, + fee_manager: _, + royalty_registry: _, marketplace, - .. }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; + let num_offers: u8 = 4; let token_id = "1".to_string(); - for idx in 1..(num_orders + 1) { - let token_bidder = - setup_additional_account(&mut router, &format!("bidder-{}", idx)).unwrap(); - - let offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let mut offer_ids: Vec = vec![]; + for idx in 1..(num_offers + 1) { + let offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( - token_bidder.clone(), + bidder.clone(), marketplace.clone(), &set_offer, - &[offer_price.clone()], + &[offer_price], ); assert!(response.is_ok()); + + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); + offer_ids.push(offer_id); } - // Other collection address returns no offers + // Other collection returns no offers let dummy_collection = Addr::unchecked("dummy_collection"); let offers = router .wrap() @@ -198,14 +151,14 @@ fn try_query_offers_by_token_price() { .unwrap(); assert_eq!(offers.len(), 0); - // Other token_ids address returns no offers + // Other token id returns no offers let offers = router .wrap() .query_wasm_smart::>( &marketplace, &QueryMsg::OffersByTokenPrice { collection: collection.to_string(), - token_id: "5".to_string(), + token_id: "2".to_string(), denom: NATIVE_DENOM.to_string(), query_options: None, }, @@ -220,15 +173,15 @@ fn try_query_offers_by_token_price() { &marketplace, &QueryMsg::OffersByTokenPrice { collection: collection.to_string(), - token_id: "5".to_string(), - denom: ATOM_DENOM.to_string(), + token_id: "1".to_string(), + denom: JUNO_DENOM.to_string(), query_options: None, }, ) .unwrap(); assert_eq!(offers.len(), 0); - // Correct number of offers returned for correct collection, token_id, and denom + // Correct number of offers returned for correct token_id and denom let offers = router .wrap() .query_wasm_smart::>( @@ -241,10 +194,10 @@ fn try_query_offers_by_token_price() { }, ) .unwrap(); - assert_eq!(offers.len(), num_orders as usize); + assert_eq!(offers.len(), num_offers as usize); // Query Options work - let offers = router + let qo_offers = router .wrap() .query_wasm_smart::>( &marketplace, @@ -255,31 +208,29 @@ fn try_query_offers_by_token_price() { query_options: Some(QueryOptions { descending: Some(true), limit: None, - min: Some(QueryBound::Exclusive(OffersByTokenPriceOffset { - amount: native_denom_price_range.min.u128() + 2, - creator: "".to_string(), + min: Some(QueryBound::Exclusive(PriceOffset { + id: offers[0].id.clone(), + amount: offers[0].details.price.amount.u128(), })), - max: Some(QueryBound::Exclusive(OffersByTokenPriceOffset { - amount: native_denom_price_range.min.u128() + 5, - creator: "".to_string(), + max: Some(QueryBound::Exclusive(PriceOffset { + id: offers[3].id.clone(), + amount: offers[3].details.price.amount.u128(), })), }), }, ) .unwrap(); - assert_eq!(offers.len(), 3); - assert_eq!( - offers[0].order_info.price, - coin(native_denom_price_range.min.u128() + 4, NATIVE_DENOM) - ); - assert_eq!( - offers[1].order_info.price, - coin(native_denom_price_range.min.u128() + 3, NATIVE_DENOM) - ); - assert_eq!( - offers[2].order_info.price, - coin(native_denom_price_range.min.u128() + 2, NATIVE_DENOM) - ); + + assert_eq!(qo_offers.len(), 2); + + for (idx, offer) in qo_offers.iter().enumerate() { + let offer_idx = 2 - idx; + assert_eq!(offer.id, offers[offer_idx].id); + assert_eq!( + offer.details.price.amount.u128(), + offers[offer_idx].details.price.amount.u128() + ); + } } #[test] @@ -288,60 +239,61 @@ fn try_query_offers_by_creator() { minter_template: MinterTemplateResponse { mut router, - accts: MarketAccounts { bidder, .. }, + accts: + MarketAccounts { + creator: _, + owner, + bidder, + fee_manager: _, + }, .. }, contracts: TestContracts { collection, + minter: _, + fee_manager: _, + royalty_registry: _, marketplace, - .. }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { - let token_id = idx.to_string(); - let offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); + let num_offers: u8 = 4; + let token_id = "1".to_string(); + let mut offer_ids: Vec = vec![]; + for idx in 1..(num_offers + 1) { + let offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_offer, - &[offer_price.clone()], + &[offer_price], ); assert!(response.is_ok()); + + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); + offer_ids.push(offer_id); } // Other creator address returns no offers - let dummy_creator = Addr::unchecked("dummy_creator"); let offers = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::OffersByCreator { - creator: dummy_creator.to_string(), + &QueryMsg::OffersByCreatorCollection { + creator: owner.to_string(), + collection: collection.to_string(), query_options: None, }, ) @@ -353,153 +305,41 @@ fn try_query_offers_by_creator() { .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::OffersByCreator { + &QueryMsg::OffersByCreatorCollection { creator: bidder.to_string(), + collection: collection.to_string(), query_options: None, }, ) .unwrap(); - assert_eq!(offers.len(), num_orders as usize); + assert_eq!(offers.len(), num_offers as usize); // Query Options work - let offers = router + let qo_offers = router .wrap() .query_wasm_smart::>( &marketplace, - &QueryMsg::OffersByCreator { + &QueryMsg::OffersByCreatorCollection { creator: bidder.to_string(), + collection: collection.to_string(), query_options: Some(QueryOptions { descending: Some(true), limit: Some(2), - min: Some(QueryBound::Inclusive(OffersByCreatorOffset { - collection: "".to_string(), - token_id: "".to_string(), - })), - max: Some(QueryBound::Exclusive(OffersByCreatorOffset { - collection: collection.to_string(), - token_id: "5".to_string(), - })), + min: Some(QueryBound::Exclusive(offers[0].id.clone())), + max: Some(QueryBound::Exclusive(offers[3].id.clone())), }), }, ) .unwrap(); - assert_eq!(offers.len(), 2); - assert_eq!(offers[0].token_id, "4".to_string()); - assert_eq!(offers[1].token_id, "3".to_string()); -} - -#[test] -fn try_query_offers_by_expiration() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: MarketAccounts { bidder, .. }, - .. - }, - contracts: - TestContracts { - collection, - marketplace, - .. - }, - } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; + assert_eq!(qo_offers.len(), 2); - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let num_orders: u8 = 4; - for idx in 1..(num_orders + 1) { - let token_id = idx.to_string(); - let offer_price = coin( - native_denom_price_range.min.u128() + idx as u128, - NATIVE_DENOM, - ); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(idx as u64), - removal_reward: config.min_removal_reward.clone(), - }), - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[offer_price.clone(), config.min_removal_reward.clone()], + for (idx, offer) in qo_offers.iter().enumerate() { + let offer_idx = 2 - idx; + assert_eq!(offer.id, offers[offer_idx].id); + assert_eq!( + offer.details.price.amount.u128(), + offers[offer_idx].details.price.amount.u128() ); - assert!(response.is_ok()); } - - // Correct number of offers returned for correct query - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByExpiration { - query_options: None, - }, - ) - .unwrap(); - assert_eq!(offers.len(), num_orders as usize); - - // Query Options work - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByExpiration { - query_options: Some(QueryOptions { - descending: Some(true), - limit: Some(2), - min: Some(QueryBound::Exclusive(OffersByExpirationOffset { - collection: "".to_string(), - token_id: "".to_string(), - creator: "".to_string(), - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(3u64) - .seconds(), - })), - max: Some(QueryBound::Exclusive(OffersByExpirationOffset { - collection: "".to_string(), - token_id: "".to_string(), - creator: "".to_string(), - expiration: block_time - .plus_seconds(config.min_expiration_seconds) - .plus_seconds(5u64) - .seconds(), - })), - }), - }, - ) - .unwrap(); - - assert_eq!(offers.len(), 2); - assert_eq!(offers[0].token_id, "4".to_string()); - assert_eq!(offers[1].token_id, "3".to_string()); } diff --git a/contracts/marketplace-v2/src/testing/tests/offers.rs b/contracts/marketplace-v2/src/testing/tests/offers.rs index 297b678c..9dac3a5a 100644 --- a/contracts/marketplace-v2/src/testing/tests/offers.rs +++ b/contracts/marketplace-v2/src/testing/tests/offers.rs @@ -1,16 +1,12 @@ use crate::{ - msg::{ExecuteMsg, OrderOptions, QueryMsg, UpdateVal}, - state::{Config, ExpirationInfo, KeyString, Offer, PriceRange}, + msg::{ExecuteMsg, QueryMsg}, + orders::{Offer, OrderDetails}, testing::{ - helpers::{ - marketplace::mint_and_set_ask, - nft_functions::{approve, mint_for}, - utils::assert_error, - }, + helpers::utils::{assert_error, find_attrs}, setup::{ msg::MarketAccounts, setup_accounts::setup_additional_account, - setup_marketplace::{ATOM_DENOM, JUNO_DENOM}, + setup_marketplace::JUNO_DENOM, templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, }, }, @@ -18,481 +14,102 @@ use crate::{ }; use cosmwasm_std::coin; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; use cw_multi_test::Executor; use cw_utils::NativeBalance; use sg_marketplace_common::MarketplaceStdError; -use sg_std::GENESIS_MINT_START_TIME; use sg_std::NATIVE_DENOM; use std::ops::{Add, Sub}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; +use test_suite::common_setup::msg::MinterTemplateResponse; #[test] -fn try_set_simple_offer() { +fn try_set_offer() { let MarketplaceV2Template { minter_template: MinterTemplateResponse { mut router, accts: MarketAccounts { - creator, - owner, + creator: _, + owner: _, bidder, + fee_manager: _, }, .. }, contracts: TestContracts { collection, - minter, - fair_burn: _, + minter: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let _bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - let token_id = "1"; - mint_for(&mut router, &creator, &owner, &minter, token_id); // Create offer without sufficient offer funds fails - let offer_price = coin(2_000_000, NATIVE_DENOM); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InsufficientFunds { - expected: offer_price, - } - .to_string(), - ); - - // Create offer with invalid denom fails - let offer_price = coin(2_000_000, JUNO_DENOM); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[offer_price], - ); - assert_error( - response, - ContractError::InvalidInput("invalid denom".to_string()).to_string(), - ); - - // Create offer with price too low fails - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128() - 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[config.listing_fee.clone()], - ); - assert_error( - response, - ContractError::InvalidInput("price too low 99999 < 100000".to_string()).to_string(), - ); - - // Create ask with price too high fails - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.max.u128() + 1u128, NATIVE_DENOM), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[config.listing_fee], - ); - assert_error( - response, - ContractError::InvalidInput("price too high 100000000000001 > 100000000000000".to_string()) - .to_string(), - ); - - // Create simple offer succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }; - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[price.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - assert_eq!( - bidder_native_balances_before.sub(price.clone()).unwrap(), - bidder_native_balances_after - ); - - let offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!(offer.order_info.price, price); - assert_eq!(offer.order_info.creator, bidder); - assert_eq!(offer.order_info.asset_recipient, None); - assert_eq!(offer.order_info.finders_fee_bps, None); - assert_eq!(offer.order_info.expiration_info, None); - - // Create duplicate offer fails - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[price.clone()], - ); - assert_error( - response, - ContractError::EntityExists(format!( - "offer {}", - Offer::build_key(&collection, &token_id.to_string(), &bidder).to_string() - )) - .to_string(), - ); - - // Overpay listing fee succeeds - let owner_native_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); + let offer_price = coin(1_000_000, NATIVE_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }; - let response = router.execute_contract( - owner.clone(), - marketplace, - &set_offer, - &[coin(price.amount.u128() * 2u128, NATIVE_DENOM)], - ); - assert!(response.is_ok()); - - let owner_native_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_native_balances_before.sub(price).unwrap(), - owner_native_balances_after - ); -} - -#[test] -pub fn try_set_complex_offer() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); - let finder = setup_additional_account(&mut router, "finder").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let token_id = "1"; - mint_for(&mut router, &creator, &owner, &minter, token_id); - approve(&mut router, &owner, &collection, &marketplace, token_id); - - // Create offer with finder sender fails - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(bidder.to_string()), - finders_fee_bps: None, - expiration_info: None, - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[coin(native_denom_price_range.min.u128(), NATIVE_DENOM)], - ); - assert_error( - response, - ContractError::InvalidInput("finder should not be sender".to_string()).to_string(), - ); - - // Create offer with finders fee above 100% fails - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(10001), - expiration_info: None, - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput("finders_fee_bps is above 100%".to_string()).to_string(), - ); - - // Create offer with expiration info fails when no fee is paid - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { + details: OrderDetails { + price: offer_price.clone(), asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(1000), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), JUNO_DENOM), - }), - }), + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_offer, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], - ); - assert_error( - response, - ContractError::InvalidInput(format!( - "removal reward must be at least {}", - config.min_removal_reward - )) - .to_string(), + &[coin(offer_price.amount.u128() - 1u128, NATIVE_DENOM)], ); + assert_error(response, ContractError::InsufficientFunds.to_string()); - // Create offer with expiration too soon fails + // Create offer with invalid denom fails + let offer_price = coin(1_000_000, JUNO_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: coin(native_denom_price_range.min.u128(), NATIVE_DENOM), - order_options: Some(OrderOptions { + details: OrderDetails { + price: offer_price.clone(), asset_recipient: None, - finder: Some(finder.to_string()), - finders_fee_bps: Some(1000), - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds - 1), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }), - }), + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_offer, - &[coin( - config.listing_fee.amount.u128(), - &config.listing_fee.denom, - )], + &[coin(offer_price.amount.u128() - 1u128, JUNO_DENOM)], ); assert_error( response, - ContractError::InvalidInput("expiration is below minimum".to_string()).to_string(), + ContractError::InvalidInput("invalid denom".to_string()).to_string(), ); - // Create offer with all valid parameters succeeds - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - let finders_fee_bps = 1000; - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; + // Create offer succeeds, even when overpaid + let asset_recipient = Addr::unchecked("asset_recipient".to_string()); + let finder = Addr::unchecked("finder".to_string()); + let offer_price = coin(1_000_000, NATIVE_DENOM); let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { + details: OrderDetails { + price: offer_price.clone(), asset_recipient: Some(asset_recipient.to_string()), finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; - - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[coin( - (config.listing_fee.amount + config.min_removal_reward.amount).u128(), - &config.listing_fee.denom, - )], - ); - assert!(response.is_ok()); - - let offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!(offer.order_info.price, price); - assert_eq!(offer.order_info.creator, bidder); - assert_eq!( - offer.order_info.asset_recipient, - Some(asset_recipient.clone()) - ); - assert_eq!(offer.order_info.finders_fee_bps, Some(finders_fee_bps)); - assert_eq!(offer.order_info.expiration_info, Some(expiration_info)); - - // Create ask with high removal reward succeeds - let token_id = "2"; - let removal_reward_amount = config.min_removal_reward.amount * Uint128::from(10u8); - let overpay_fees = vec![ - price.clone(), - coin( - removal_reward_amount.u128(), - &config.min_removal_reward.denom, - ), - coin(config.listing_fee.amount.u128(), ATOM_DENOM), - ]; - - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin( - removal_reward_amount.u128(), - &config.min_removal_reward.denom, - ), + }, }; - let bidder_native_balances_before = NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: Some(asset_recipient.to_string()), - finder: Some(finder.to_string()), - finders_fee_bps: Some(finders_fee_bps), - expiration_info: Some(expiration_info.clone()), - }), - }; let response = router.execute_contract( bidder.clone(), marketplace.clone(), &set_offer, - &overpay_fees, + &[coin(offer_price.amount.u128() * 2u128, NATIVE_DENOM)], ); assert!(response.is_ok()); @@ -500,30 +117,28 @@ pub fn try_set_complex_offer() { NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); assert_eq!( bidder_native_balances_before - .sub(price.clone()) - .unwrap() - .sub(coin(removal_reward_amount.u128(), NATIVE_DENOM)) + .sub(offer_price.clone()) .unwrap(), bidder_native_balances_after ); + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); + let offer = router .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) + .query_wasm_smart::>(&marketplace, &QueryMsg::Offer(offer_id.clone())) .unwrap() .unwrap(); - assert_eq!(offer.order_info.price, price); - assert_eq!(offer.order_info.creator, bidder); - assert_eq!(offer.order_info.asset_recipient, Some(asset_recipient)); - assert_eq!(offer.order_info.finders_fee_bps, Some(finders_fee_bps)); - assert_eq!(offer.order_info.expiration_info, Some(expiration_info)); + + assert_eq!(offer.id, offer_id); + assert_eq!(offer.creator, bidder); + assert_eq!(offer.collection, collection); + assert_eq!(offer.token_id, token_id); + assert_eq!(offer.details.price, offer_price); + assert_eq!(offer.details.asset_recipient, Some(asset_recipient)); + assert_eq!(offer.details.finder, Some(finder)); } #[test] @@ -535,8 +150,9 @@ pub fn try_update_offer() { accts: MarketAccounts { creator: _, - owner: _, + owner, bidder, + fee_manager: _, }, .. }, @@ -544,139 +160,70 @@ pub fn try_update_offer() { TestContracts { collection, minter: _, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - let asset_recipient2 = setup_additional_account(&mut router, "asset_recipient2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); + let asset_recipient = setup_additional_account(&mut router, "asset_recipient").unwrap(); + let finder = setup_additional_account(&mut router, "finder").unwrap(); - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), + let num_offers: u8 = 4; + let token_id = "1".to_string(); + let mut offer_ids: Vec = vec![]; + for idx in 1..(num_offers + 1) { + let offer_price = coin(1000000u128 + idx as u128, NATIVE_DENOM); + let set_offer = ExecuteMsg::SetOffer { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, }, - ) - .unwrap(); - - let token_id = "1"; - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - router - .execute_contract( + }; + let response = router.execute_contract( bidder.clone(), marketplace.clone(), - &ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }, - &[price], - ) - .unwrap(); - - // Setting asset_recipient and finders_fee_bps succeeds + &set_offer, + &[offer_price], + ); + assert!(response.is_ok()); + + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); + offer_ids.push(offer_id); + } + + // Non creator updating offer fails let update_offer = ExecuteMsg::UpdateOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: Some(UpdateVal::Set(asset_recipient2.to_string())), - finders_fee_bps: Some(UpdateVal::Set(2000)), - expiration_info: None, - }; - let response = router.execute_contract(bidder.clone(), marketplace.clone(), &update_offer, &[]); - assert!(response.is_ok()); - - let offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap() - .unwrap(); - assert_eq!(offer.order_info.asset_recipient, Some(asset_recipient2)); - assert_eq!(offer.order_info.finders_fee_bps, Some(2000)); - - // Setting expiration_info without fee fails - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }; - let update_offer = ExecuteMsg::UpdateOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), + id: offer_ids[0].clone(), + details: OrderDetails { + price: coin(1000000u128, NATIVE_DENOM), + asset_recipient: Some(asset_recipient.to_string()), + finder: Some(finder.to_string()), + }, }; - - let response = router.execute_contract(bidder.clone(), marketplace.clone(), &update_offer, &[]); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &update_offer, &[]); assert_error( response, - ContractError::InsufficientFunds { - expected: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - } + MarketplaceStdError::Unauthorized( + "only the creator of offer can perform this action".to_string(), + ) .to_string(), ); - // Setting expiration_info with fee succeeds - let update_offer = ExecuteMsg::UpdateOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info)), - }; - - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &update_offer, - &[config.min_removal_reward.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - - assert_eq!( - bidder_native_balances_before - .sub(config.min_removal_reward.clone()) - .unwrap(), - bidder_native_balances_after - ); - - // Updating expiration_info refunds previous paid removal reward - let expiration_info = ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin( - config.min_removal_reward.amount.u128() * 2u128, - NATIVE_DENOM, - ), - }; + // Updating offer succeeds, wallet is refunded + let new_price = coin(1000000u128, NATIVE_DENOM); let update_offer = ExecuteMsg::UpdateOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Set(expiration_info.clone())), + id: offer_ids[0].clone(), + details: OrderDetails { + price: new_price.clone(), + asset_recipient: None, + finder: None, + }, }; let bidder_native_balances_before = @@ -685,44 +232,14 @@ pub fn try_update_offer() { bidder.clone(), marketplace.clone(), &update_offer, - &[expiration_info.removal_reward.clone()], + &[new_price], ); assert!(response.is_ok()); - let bidder_native_balances_after = NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); assert_eq!( - bidder_native_balances_before - .sub(config.min_removal_reward) - .unwrap(), - bidder_native_balances_after - ); - - // Removing expiration_info refunds removal reward - let update_offer = ExecuteMsg::UpdateOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - asset_recipient: None, - finders_fee_bps: None, - expiration_info: Some(UpdateVal::Unset), - }; - - let bidder_native_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - let response = router.execute_contract( - bidder.clone(), - marketplace, - &update_offer, - &[expiration_info.removal_reward.clone()], - ); - assert!(response.is_ok()); - - let bidder_native_balances_after = - NativeBalance(router.wrap().query_all_balances(bidder).unwrap()); - - assert_eq!( - bidder_native_balances_before.add(expiration_info.removal_reward), + bidder_native_balances_before.add(coin(1u128, NATIVE_DENOM).clone()), bidder_native_balances_after ); } @@ -735,17 +252,18 @@ pub fn try_remove_offer() { mut router, accts: MarketAccounts { - creator, - owner, + creator: _, + owner: _, bidder, + fee_manager: _, }, .. }, contracts: TestContracts { collection, - minter, - fair_burn: _, + minter: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -753,195 +271,48 @@ pub fn try_remove_offer() { let bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - let token_id = "1"; - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: None, - }, - &[price], - ) - .unwrap(); - - // Removing offer as creator succeeds - let remove_offer = ExecuteMsg::RemoveOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - }; - let response = router.execute_contract(bidder.clone(), marketplace.clone(), &remove_offer, &[]); - assert!(response.is_ok()); - - let offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap(); - assert!(offer.is_none()); - - // Reject offer as non NFT holder fails - let token_id = "2"; - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - token_id, - &coin(native_denom_price_range.max.u128(), NATIVE_DENOM), - &[config.listing_fee], - None, - ); - let price = coin(native_denom_price_range.min.u128(), NATIVE_DENOM); - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), + let price = coin(1000000u128, NATIVE_DENOM); + let response = router.execute_contract( + bidder.clone(), + marketplace.clone(), + &ExecuteMsg::SetOffer { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { price: price.clone(), - order_options: None, + asset_recipient: None, + finder: None, }, - &[price.clone()], - ) - .unwrap(); - - let reject_offer = ExecuteMsg::RejectOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }; - let response = - router.execute_contract(bidder2.clone(), marketplace.clone(), &reject_offer, &[]); - assert_error( - response, - MarketplaceStdError::Unauthorized("sender is not owner or seller".to_string()).to_string(), + }, + &[price], ); - // Reject offer as NFT holder succeeds - let reject_offer = ExecuteMsg::RejectOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }; - let response = router.execute_contract(owner.clone(), marketplace.clone(), &reject_offer, &[]); - assert!(response.is_ok()); - - let offer = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) - .unwrap(); - assert!(offer.is_none()); - - // Cannot remove offer that is not expired - router - .execute_contract( - bidder.clone(), - marketplace.clone(), - &ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: coin(config.min_removal_reward.amount.u128(), NATIVE_DENOM), - }), - }), - }, - &[price, config.min_removal_reward.clone()], - ) + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() .unwrap(); - let remove_expired_offer = ExecuteMsg::RemoveExpiredOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), + // Removing offer as non creator fails + let remove_offer = ExecuteMsg::RemoveOffer { + id: offer_id.clone(), }; let response = - router.execute_contract(bidder2, marketplace.clone(), &remove_expired_offer, &[]); - + router.execute_contract(bidder2.clone(), marketplace.clone(), &remove_offer, &[]); assert_error( response, - ContractError::EntityNotExpired(format!( - "offer {}", - Offer::build_key(&collection, &token_id.to_string(), &bidder).to_string() - )) + MarketplaceStdError::Unauthorized( + "only the creator of offer can perform this action".to_string(), + ) .to_string(), ); - // Anyone can remove ask that is expired - setup_block_time( - &mut router, - block_time - .plus_seconds(config.min_expiration_seconds) - .nanos(), - None, - ); - let remove_expired_offer = ExecuteMsg::RemoveExpiredOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &remove_expired_offer, - &[], - ); + // Removing offer as creator succeeds + let response = router.execute_contract(bidder.clone(), marketplace.clone(), &remove_offer, &[]); assert!(response.is_ok()); let offer = router .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::Offer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - }, - ) + .query_wasm_smart::>(&marketplace, &QueryMsg::Offer(offer_id)) .unwrap(); assert!(offer.is_none()); } diff --git a/contracts/marketplace-v2/src/testing/tests/sales.rs b/contracts/marketplace-v2/src/testing/tests/sales.rs index 3a6687f8..d4b81685 100644 --- a/contracts/marketplace-v2/src/testing/tests/sales.rs +++ b/contracts/marketplace-v2/src/testing/tests/sales.rs @@ -1,8 +1,12 @@ use crate::{ msg::{ExecuteMsg, QueryMsg}, + orders::OrderDetails, state::Config, testing::{ - helpers::nft_functions::{approve, mint_for}, + helpers::{ + nft_functions::{approve, mint_for}, + utils::find_attrs, + }, setup::{ msg::MarketAccounts, setup_accounts::setup_additional_account, @@ -34,6 +38,7 @@ fn try_set_ask_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -41,7 +46,7 @@ fn try_set_ask_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -49,9 +54,6 @@ fn try_set_ask_sale() { let bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - let config: Config = router .wrap() .query_wasm_smart(&marketplace, &QueryMsg::Config {}) @@ -87,8 +89,11 @@ fn try_set_ask_sale() { let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price_1.clone(), - order_options: None, + details: OrderDetails { + price: offer_price_1.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract(bidder, marketplace.clone(), &set_offer, &[offer_price_1]); @@ -99,8 +104,11 @@ fn try_set_ask_sale() { let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price_2.clone(), - order_options: None, + details: OrderDetails { + price: offer_price_2.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder2.clone(), @@ -116,15 +124,13 @@ fn try_set_ask_sale() { let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: coin(5_000_000, NATIVE_DENOM), - order_options: None, + details: OrderDetails { + price: coin(5_000_000, NATIVE_DENOM), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); let owner_balances_after = @@ -140,15 +146,12 @@ fn try_set_ask_sale() { let sale_coin = offer_price_2; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); assert_eq!( - owner_balances_before - .sub(config.listing_fee) - .unwrap() - .add(coin(seller_amount.u128(), NATIVE_DENOM)), + owner_balances_before.add(coin(seller_amount.u128(), NATIVE_DENOM)), owner_balances_after ); assert_eq!( @@ -172,6 +175,7 @@ fn try_accept_ask_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -179,15 +183,12 @@ fn try_accept_ask_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - let config: Config = router .wrap() .query_wasm_smart(&marketplace, &QueryMsg::Config {}) @@ -223,22 +224,23 @@ fn try_accept_ask_sale() { let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: ask_price.clone(), - order_options: None, + details: OrderDetails { + price: ask_price.clone(), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); + let ask_id = find_attrs(response.unwrap(), "wasm-set-ask", "id") + .pop() + .unwrap(); // Accept ask directly let accept_ask = ExecuteMsg::AcceptAsk { - collection: collection.to_string(), - token_id: token_id.to_string(), - order_options: None, + id: ask_id, + asset_recipient: None, + finder: None, }; let response = router.execute_contract( bidder.clone(), @@ -261,15 +263,12 @@ fn try_accept_ask_sale() { let sale_coin = ask_price; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); assert_eq!( - owner_balances_before - .sub(config.listing_fee) - .unwrap() - .add(coin(seller_amount.u128(), NATIVE_DENOM)), + owner_balances_before.add(coin(seller_amount.u128(), NATIVE_DENOM)), owner_balances_after ); assert_eq!( @@ -293,6 +292,7 @@ fn try_set_offer_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -300,15 +300,12 @@ fn try_set_offer_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - let config: Config = router .wrap() .query_wasm_smart(&marketplace, &QueryMsg::Config {}) @@ -344,15 +341,13 @@ fn try_set_offer_sale() { let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: ask_price.clone(), - order_options: None, + details: OrderDetails { + price: ask_price.clone(), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); // Create offer that matches ask @@ -360,8 +355,11 @@ fn try_set_offer_sale() { let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), @@ -384,15 +382,12 @@ fn try_set_offer_sale() { let sale_coin = ask_price; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); assert_eq!( - owner_balances_before - .sub(config.listing_fee) - .unwrap() - .add(coin(seller_amount.u128(), NATIVE_DENOM)), + owner_balances_before.add(coin(seller_amount.u128(), NATIVE_DENOM)), owner_balances_after ); assert_eq!( @@ -416,6 +411,7 @@ fn try_accept_offer_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -423,15 +419,12 @@ fn try_accept_offer_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - let config: Config = router .wrap() .query_wasm_smart(&marketplace, &QueryMsg::Config {}) @@ -465,8 +458,11 @@ fn try_accept_offer_sale() { let set_offer = ExecuteMsg::SetOffer { collection: collection.to_string(), token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), @@ -475,15 +471,17 @@ fn try_accept_offer_sale() { &[offer_price.clone()], ); assert!(response.is_ok()); + let offer_id = find_attrs(response.unwrap(), "wasm-set-offer", "id") + .pop() + .unwrap(); mint_for(&mut router, &creator, &owner, &minter, token_id); approve(&mut router, &owner, &collection, &marketplace, token_id); let accept_offer = ExecuteMsg::AcceptOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - creator: bidder.to_string(), - order_options: None, + id: offer_id, + asset_recipient: None, + finder: None, }; let response = router.execute_contract(owner.clone(), marketplace.clone(), &accept_offer, &[]); assert!(response.is_ok()); @@ -501,7 +499,7 @@ fn try_accept_offer_sale() { let sale_coin = offer_price; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); @@ -530,6 +528,7 @@ fn try_set_collection_offer_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -537,7 +536,7 @@ fn try_set_collection_offer_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, @@ -581,23 +580,24 @@ fn try_set_collection_offer_sale() { let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: ask_price.clone(), - order_options: None, + details: OrderDetails { + price: ask_price.clone(), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); // Create offer that matches ask let offer_price = coin(10_000_000, NATIVE_DENOM); let set_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: ask_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), @@ -620,15 +620,12 @@ fn try_set_collection_offer_sale() { let sale_coin = ask_price; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); assert_eq!( - owner_balances_before - .sub(config.listing_fee) - .unwrap() - .add(coin(seller_amount.u128(), NATIVE_DENOM)), + owner_balances_before.add(coin(seller_amount.u128(), NATIVE_DENOM)), owner_balances_after ); assert_eq!( @@ -652,6 +649,7 @@ fn try_accept_collection_offer_sale() { creator, owner, bidder, + fee_manager: _, }, .. }, @@ -659,15 +657,12 @@ fn try_accept_collection_offer_sale() { TestContracts { collection, minter, - fair_burn: _, + fee_manager: _, royalty_registry: _, marketplace, }, } = marketplace_v2_template(10_000); - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let _block_time = router.block_info().time; - let config: Config = router .wrap() .query_wasm_smart(&marketplace, &QueryMsg::Config {}) @@ -700,8 +695,11 @@ fn try_accept_collection_offer_sale() { let offer_price = coin(10_000_000, NATIVE_DENOM); let set_offer = ExecuteMsg::SetCollectionOffer { collection: collection.to_string(), - price: offer_price.clone(), - order_options: None, + details: OrderDetails { + price: offer_price.clone(), + asset_recipient: None, + finder: None, + }, }; let response = router.execute_contract( bidder.clone(), @@ -710,6 +708,9 @@ fn try_accept_collection_offer_sale() { &[offer_price.clone()], ); assert!(response.is_ok()); + let collection_offer_id = find_attrs(response.unwrap(), "wasm-set-collection-offer", "id") + .pop() + .unwrap(); mint_for(&mut router, &creator, &owner, &minter, token_id); approve(&mut router, &owner, &collection, &marketplace, token_id); @@ -718,22 +719,20 @@ fn try_accept_collection_offer_sale() { let set_ask = ExecuteMsg::SetAsk { collection: collection.to_string(), token_id: token_id.to_string(), - price: coin(20_000_000, NATIVE_DENOM), - order_options: None, + details: OrderDetails { + price: coin(20_000_000, NATIVE_DENOM), + asset_recipient: None, + finder: None, + }, }; - let response = router.execute_contract( - owner.clone(), - marketplace.clone(), - &set_ask, - &[config.listing_fee.clone()], - ); + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); assert!(response.is_ok()); let accept_collection_offer = ExecuteMsg::AcceptCollectionOffer { - collection: collection.to_string(), + id: collection_offer_id, token_id: token_id.to_string(), - creator: bidder.to_string(), - order_options: None, + asset_recipient: None, + finder: None, }; let response = router.execute_contract( owner.clone(), @@ -756,15 +755,12 @@ fn try_accept_collection_offer_sale() { let sale_coin = offer_price; let fair_burn_amount = sale_coin .amount - .mul_ceil(Decimal::bps(config.trading_fee_bps)); + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); let seller_amount = sale_coin.amount.sub(fair_burn_amount).sub(royalty_amount); assert_eq!( - owner_balances_before - .sub(config.listing_fee) - .unwrap() - .add(coin(seller_amount.u128(), NATIVE_DENOM)), + owner_balances_before.add(coin(seller_amount.u128(), NATIVE_DENOM)), owner_balances_after ); assert_eq!( @@ -776,3 +772,189 @@ fn try_accept_collection_offer_sale() { royalty_balances_after ); } + +#[test] +fn try_sale_fee_breakdown() { + let MarketplaceV2Template { + minter_template: + MinterTemplateResponse { + mut router, + accts: + MarketAccounts { + creator, + owner, + bidder, + fee_manager, + }, + .. + }, + contracts: + TestContracts { + collection, + minter, + fee_manager: _, + royalty_registry: _, + marketplace, + }, + } = marketplace_v2_template(10_000); + + let config: Config = router + .wrap() + .query_wasm_smart(&marketplace, &QueryMsg::Config {}) + .unwrap(); + + let royalty_info: RoyaltyInfoResponse = router + .wrap() + .query_wasm_smart::( + collection.clone(), + &Sg721QueryMsg::CollectionInfo {}, + ) + .unwrap() + .royalty_info + .unwrap(); + + let bidder_balances_before = + NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); + let royalty_balances_before = NativeBalance( + router + .wrap() + .query_all_balances(royalty_info.payment_address.clone()) + .unwrap(), + ); + + // Create ask with a finder + let maker: Addr = Addr::unchecked("maker".to_string()); + let tokens_recipient: Addr = Addr::unchecked("tokens_recipient".to_string()); + let token_id = "1"; + mint_for(&mut router, &creator, &owner, &minter, token_id); + approve(&mut router, &owner, &collection, &marketplace, token_id); + let ask_price = coin(5_000_000, NATIVE_DENOM); + + let set_ask = ExecuteMsg::SetAsk { + collection: collection.to_string(), + token_id: token_id.to_string(), + details: OrderDetails { + price: ask_price.clone(), + asset_recipient: Some(tokens_recipient.to_string()), + finder: Some(maker.to_string()), + }, + }; + let response = router.execute_contract(owner.clone(), marketplace.clone(), &set_ask, &[]); + assert!(response.is_ok()); + let ask_id = find_attrs(response.unwrap(), "wasm-set-ask", "id") + .pop() + .unwrap(); + + // Accept ask with a taker + let taker: Addr = Addr::unchecked("taker".to_string()); + let nft_recipient: Addr = Addr::unchecked("nft_recipient".to_string()); + let accept_ask = ExecuteMsg::AcceptAsk { + id: ask_id, + asset_recipient: Some(nft_recipient.to_string()), + finder: Some(taker.to_string()), + }; + let response = router.execute_contract( + bidder.clone(), + marketplace.clone(), + &accept_ask, + &[ask_price.clone()], + ); + assert!(response.is_ok()); + + // Fetch balances after sale + let fee_manager_balances_after = + NativeBalance(router.wrap().query_all_balances(fee_manager).unwrap()); + let royalty_balances_after = NativeBalance( + router + .wrap() + .query_all_balances(royalty_info.payment_address.clone()) + .unwrap(), + ); + let maker_balances_after = + NativeBalance(router.wrap().query_all_balances(maker.clone()).unwrap()); + let taker_balances_after = NativeBalance(router.wrap().query_all_balances(taker).unwrap()); + let bidder_balances_after = NativeBalance(router.wrap().query_all_balances(bidder).unwrap()); + let tokens_recipient_balances_after = NativeBalance( + router + .wrap() + .query_all_balances(tokens_recipient.clone()) + .unwrap(), + ); + + // Calculate expected balances + let sale_coin = ask_price; + let protocol_reward_total = sale_coin + .amount + .mul_ceil(Decimal::bps(config.protocol_fee_bps)); + let maker_reward = protocol_reward_total.mul_ceil(Decimal::bps(config.maker_reward_bps)); + let taker_reward = protocol_reward_total.mul_ceil(Decimal::bps(config.taker_reward_bps)); + let protocol_reward_final = protocol_reward_total.sub(maker_reward).sub(taker_reward); + let royalty_amount = sale_coin.amount.mul_ceil(royalty_info.share); + let seller_amount = sale_coin + .amount + .sub(protocol_reward_total) + .sub(royalty_amount); + + let app_response = response.unwrap(); + + // Verify protocol reward + let protocol_reward_coin = coin(protocol_reward_final.u128(), NATIVE_DENOM); + assert_eq!( + fee_manager_balances_after, + NativeBalance(vec![protocol_reward_coin.clone()]) + ); + let protocol_reward_event = find_attrs(app_response.clone(), "wasm-finalize-sale", "protocol") + .pop() + .unwrap(); + assert_eq!(protocol_reward_event, protocol_reward_coin.to_string()); + + // Verify maker reward + let maker_reward_coin = coin(maker_reward.u128(), NATIVE_DENOM); + assert_eq!( + maker_balances_after, + NativeBalance(vec![maker_reward_coin.clone()]) + ); + let maker_reward_event = find_attrs(app_response.clone(), "wasm-finalize-sale", "maker") + .pop() + .unwrap(); + assert_eq!(maker_reward_event, maker_reward_coin.to_string()); + + // Verify taker reward + let taker_reward_coin = coin(taker_reward.u128(), NATIVE_DENOM); + assert_eq!( + taker_balances_after, + NativeBalance(vec![taker_reward_coin.clone()]) + ); + let taker_reward_event = find_attrs(app_response.clone(), "wasm-finalize-sale", "taker") + .pop() + .unwrap(); + assert_eq!(taker_reward_event, taker_reward_coin.to_string()); + + // Verify royalty reward + let royalty_reward_coin = coin(royalty_amount.u128(), NATIVE_DENOM); + assert_eq!( + royalty_balances_before.add(royalty_reward_coin.clone()), + royalty_balances_after + ); + let royalty_reward_event = find_attrs(app_response.clone(), "wasm-finalize-sale", "royalty") + .pop() + .unwrap(); + assert_eq!(royalty_reward_event, royalty_reward_coin.to_string()); + + // Verify seller reward + let seller_coin = coin(seller_amount.u128(), NATIVE_DENOM); + assert_eq!( + tokens_recipient_balances_after, + NativeBalance(vec![seller_coin.clone()]) + ); + let seller_event = find_attrs(app_response.clone(), "wasm-finalize-sale", "seller") + .pop() + .unwrap(); + assert_eq!(seller_event, seller_coin.to_string()); + + // Verify bidder paid + assert_eq!( + bidder_balances_before.sub(sale_coin.clone()).unwrap(), + bidder_balances_after + ); +} diff --git a/contracts/marketplace-v2/src/testing/tests/sudo.rs b/contracts/marketplace-v2/src/testing/tests/sudo.rs deleted file mode 100644 index 44cce5c2..00000000 --- a/contracts/marketplace-v2/src/testing/tests/sudo.rs +++ /dev/null @@ -1,494 +0,0 @@ -use crate::{ - msg::{ExecuteMsg, OrderOptions, QueryMsg, SudoMsg}, - state::{Ask, CollectionOffer, Config, Denom, ExpirationInfo, Offer, PriceRange}, - testing::{ - helpers::marketplace::mint_and_set_ask, - setup::{ - msg::MarketAccounts, - setup_accounts::setup_additional_account, - templates::{marketplace_v2_template, MarketplaceV2Template, TestContracts}, - }, - }, -}; - -use cosmwasm_std::{coin, Addr, Uint128}; -use cw721::OwnerOfResponse; -use cw_multi_test::Executor; -use cw_utils::NativeBalance; -use sg721_base::msg::QueryMsg as Sg721QueryMsg; -use sg_std::GENESIS_MINT_START_TIME; -use sg_std::NATIVE_DENOM; -use std::{ops::Sub, vec}; -use test_suite::common_setup::{ - msg::MinterTemplateResponse, setup_accounts_and_block::setup_block_time, -}; - -#[test] -fn try_sudo_begin_block_noop() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: MarketAccounts { .. }, - .. - }, - contracts: - TestContracts { - fair_burn: _, - royalty_registry: _, - marketplace, - .. - }, - } = marketplace_v2_template(10_000); - - let begin_block_msg = SudoMsg::BeginBlock {}; - let response = router.wasm_sudo(marketplace, &begin_block_msg); - assert!(response.is_ok()); -} - -#[test] -fn try_sudo_end_block() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: - MarketAccounts { - creator, - owner, - bidder, - }, - .. - }, - contracts: - TestContracts { - collection, - minter, - fair_burn: _, - royalty_registry: _, - marketplace, - }, - } = marketplace_v2_template(10_000); - - let _bidder2 = setup_additional_account(&mut router, "bidder2").unwrap(); - - setup_block_time(&mut router, GENESIS_MINT_START_TIME, None); - let block_time = router.block_info().time; - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let native_denom_price_range: PriceRange = router - .wrap() - .query_wasm_smart( - &marketplace, - &QueryMsg::PriceRange { - denom: NATIVE_DENOM.to_string(), - }, - ) - .unwrap(); - - let owner_balances_before = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - let bidder_balances_before = - NativeBalance(router.wrap().query_all_balances(bidder.clone()).unwrap()); - - let num_orders: u128 = 4; - for idx in 1..(num_orders + 1) { - let ask_price = coin( - native_denom_price_range.min.u128() + idx + 100, - NATIVE_DENOM, - ); - let token_id = idx.to_string(); - mint_and_set_ask( - &mut router, - &creator, - &owner, - &minter, - &marketplace, - &collection, - &token_id.to_string(), - &ask_price, - &[ - config.listing_fee.clone(), - config.min_removal_reward.clone(), - ], - Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: config.min_removal_reward.clone(), - }), - }), - ); - - let offer_price = coin(native_denom_price_range.min.u128() + idx, NATIVE_DENOM); - let set_offer = ExecuteMsg::SetOffer { - collection: collection.to_string(), - token_id: token_id.to_string(), - price: offer_price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: config.min_removal_reward.clone(), - }), - }), - }; - let response = router.execute_contract( - bidder.clone(), - marketplace.clone(), - &set_offer, - &[offer_price.clone(), config.min_removal_reward.clone()], - ); - assert!(response.is_ok()); - - let collection_bidder = - setup_additional_account(&mut router, &format!("collection-bidder-{}", idx)).unwrap(); - let collection_offer_price = coin(native_denom_price_range.min.u128() + idx, NATIVE_DENOM); - let set_offer = ExecuteMsg::SetCollectionOffer { - collection: collection.to_string(), - price: collection_offer_price.clone(), - order_options: Some(OrderOptions { - asset_recipient: None, - finder: None, - finders_fee_bps: None, - expiration_info: Some(ExpirationInfo { - expiration: block_time.plus_seconds(config.min_expiration_seconds), - removal_reward: config.min_removal_reward.clone(), - }), - }), - }; - let response = router.execute_contract( - collection_bidder.clone(), - marketplace.clone(), - &set_offer, - &[ - collection_offer_price.clone(), - config.min_removal_reward.clone(), - ], - ); - assert!(response.is_ok()); - } - - // End block does not remove orders yet - setup_block_time( - &mut router, - block_time - .plus_seconds(config.min_expiration_seconds) - .minus_seconds(config.order_removal_lookahead_secs) - .minus_seconds(1) - .nanos(), - None, - ); - let block_time = router.block_info().time; - let end_block_msg = SudoMsg::EndBlock {}; - let response = router.wasm_sudo(marketplace.clone(), &end_block_msg); - assert!(response.is_ok()); - - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(asks.len(), num_orders as usize); - - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(offers.len(), num_orders as usize); - - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByPrice { - collection: collection.to_string(), - denom: NATIVE_DENOM.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), num_orders as usize); - - // End block removes all orders - setup_block_time(&mut router, block_time.plus_seconds(1).nanos(), None); - let _block_time = router.block_info().time; - - let end_block_msg = SudoMsg::EndBlock {}; - let response = router.wasm_sudo(marketplace.clone(), &end_block_msg); - assert!(response.is_ok()); - - let asks = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::AsksByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(asks.len(), 0_usize); - - let offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::OffersByCollection { - collection: collection.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(offers.len(), 0_usize); - - let collection_offers = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::CollectionOffersByPrice { - collection: collection.to_string(), - denom: NATIVE_DENOM.to_string(), - query_options: None, - }, - ) - .unwrap(); - assert_eq!(collection_offers.len(), 0_usize); - - // Validate that the tokens were returned to the original owners - let owner_balances_after = - NativeBalance(router.wrap().query_all_balances(owner.clone()).unwrap()); - assert_eq!( - owner_balances_before - .sub(coin( - (config.listing_fee.amount.u128() + config.min_removal_reward.amount.u128()) - * num_orders, - NATIVE_DENOM - )) - .unwrap(), - owner_balances_after - ); - - let bidder_balances_after = NativeBalance(router.wrap().query_all_balances(bidder).unwrap()); - assert_eq!( - bidder_balances_before - .sub(coin( - config.min_removal_reward.amount.u128() * num_orders, - NATIVE_DENOM - )) - .unwrap(), - bidder_balances_after - ); - - let initial_balance = setup_additional_account(&mut router, "inital-balance").unwrap(); - let initial_balances = - NativeBalance(router.wrap().query_all_balances(initial_balance).unwrap()); - for idx in 1..(num_orders + 1) { - let collection_bidder = Addr::unchecked(format!("collection-bidder-{}", idx)); - let collection_bidder_balances_after = NativeBalance( - router - .wrap() - .query_all_balances(collection_bidder.clone()) - .unwrap(), - ); - assert_eq!( - initial_balances - .clone() - .sub(config.min_removal_reward.clone()) - .unwrap(), - collection_bidder_balances_after - ); - } - - // Validate that the NFTs were returned to the seller - for idx in 1..(num_orders + 1) { - let token_id = idx.to_string(); - let owner_of_response = router - .wrap() - .query_wasm_smart::( - &collection, - &Sg721QueryMsg::OwnerOf { - token_id: token_id.clone(), - include_expired: None, - }, - ) - .unwrap(); - assert_eq!(owner_of_response.owner, owner); - } -} - -#[test] -fn try_sudo_update_params() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: MarketAccounts { .. }, - .. - }, - contracts: - TestContracts { - fair_burn: _, - royalty_registry: _, - marketplace, - .. - }, - } = marketplace_v2_template(10_000); - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - - let delta = 1u64; - let fair_burn = "fair-burn-test".to_string(); - let listing_fee = coin( - config.listing_fee.amount.u128() + delta as u128, - config.listing_fee.denom.clone(), - ); - let min_removal_reward = coin( - config.min_removal_reward.amount.u128() + delta as u128, - config.min_removal_reward.denom.clone(), - ); - let trading_fee_bps = config.trading_fee_bps + delta; - let max_royalty_fee_bps = config.max_royalty_fee_bps + delta; - let max_finders_fee_bps = config.max_finders_fee_bps + delta; - let min_expiration_seconds = config.min_expiration_seconds + delta; - let max_asks_removed_per_block = config.max_asks_removed_per_block + delta as u32; - let max_offers_removed_per_block = config.max_offers_removed_per_block + delta as u32; - let max_collection_offers_removed_per_block = - config.max_collection_offers_removed_per_block + delta as u32; - - let end_block_msg = SudoMsg::UpdateParams { - fair_burn: Some(fair_burn.clone()), - listing_fee: Some(listing_fee.clone()), - min_removal_reward: Some(min_removal_reward.clone()), - trading_fee_bps: Some(trading_fee_bps), - max_royalty_fee_bps: Some(max_royalty_fee_bps), - max_finders_fee_bps: Some(max_finders_fee_bps), - min_expiration_seconds: Some(min_expiration_seconds), - max_asks_removed_per_block: Some(max_asks_removed_per_block), - max_offers_removed_per_block: Some(max_offers_removed_per_block), - max_collection_offers_removed_per_block: Some(max_collection_offers_removed_per_block), - }; - let response = router.wasm_sudo(marketplace.clone(), &end_block_msg); - assert!(response.is_ok()); - - let config: Config = router - .wrap() - .query_wasm_smart(&marketplace, &QueryMsg::Config {}) - .unwrap(); - assert_eq!(config.fair_burn, fair_burn); - assert_eq!(config.listing_fee, listing_fee); - assert_eq!(config.min_removal_reward, min_removal_reward); - assert_eq!(config.trading_fee_bps, trading_fee_bps); - assert_eq!(config.max_royalty_fee_bps, max_royalty_fee_bps); - assert_eq!(config.max_finders_fee_bps, max_finders_fee_bps); - assert_eq!(config.min_expiration_seconds, min_expiration_seconds); - assert_eq!( - config.max_asks_removed_per_block, - max_asks_removed_per_block - ); - assert_eq!( - config.max_offers_removed_per_block, - max_offers_removed_per_block - ); - assert_eq!( - config.max_collection_offers_removed_per_block, - max_collection_offers_removed_per_block - ); -} - -#[test] -fn try_sudo_add_remove_denoms() { - let MarketplaceV2Template { - minter_template: - MinterTemplateResponse { - mut router, - accts: MarketAccounts { .. }, - .. - }, - contracts: - TestContracts { - fair_burn: _, - royalty_registry: _, - marketplace, - .. - }, - } = marketplace_v2_template(10_000); - - let price_ranges = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::PriceRanges { - query_options: None, - }, - ) - .unwrap(); - assert_eq!(price_ranges.len(), 2_usize); - - let new_denom_price_range = ( - "uosmo".to_string(), - PriceRange { - min: Uint128::from(100u128), - max: Uint128::from(10_000u128), - }, - ); - - let add_denoms_msg = SudoMsg::AddDenoms { - price_ranges: vec![new_denom_price_range.clone()], - }; - let response = router.wasm_sudo(marketplace.clone(), &add_denoms_msg); - assert!(response.is_ok()); - - let price_ranges = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::PriceRanges { - query_options: None, - }, - ) - .unwrap(); - assert_eq!(price_ranges.len(), 3_usize); - assert!(price_ranges - .iter() - .any(|denom_price_range| denom_price_range == &new_denom_price_range)); - - let remove_denoms_msg = SudoMsg::RemoveDenoms { - denoms: vec![new_denom_price_range.0], - }; - let response = router.wasm_sudo(marketplace.clone(), &remove_denoms_msg); - assert!(response.is_ok()); - - let price_ranges = router - .wrap() - .query_wasm_smart::>( - &marketplace, - &QueryMsg::PriceRanges { - query_options: None, - }, - ) - .unwrap(); - assert_eq!(price_ranges.len(), 2_usize); -} diff --git a/contracts/reserve-auction/Cargo.toml b/contracts/reserve-auction/Cargo.toml index 48f61e8f..0e61d007 100644 --- a/contracts/reserve-auction/Cargo.toml +++ b/contracts/reserve-auction/Cargo.toml @@ -34,10 +34,10 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -sg-marketplace-common = { version = "1.1.0" } +sg-marketplace-common = { version = "= 1.1.0" } stargaze-fair-burn = { version = "1.0.2", features = ["library"] } -cosmwasm-schema = "1.2.1" -cosmwasm-std = "1.2.1" +cosmwasm-std = "1.5.3" +cosmwasm-schema = "1.5.3" cw-storage-macro = "0.16.0" cw-storage-plus = "0.16.0" cw-utils = "0.16.0" diff --git a/contracts/reserve-auction/src/query.rs b/contracts/reserve-auction/src/query.rs index 8254343c..51842c97 100644 --- a/contracts/reserve-auction/src/query.rs +++ b/contracts/reserve-auction/src/query.rs @@ -2,7 +2,7 @@ use crate::msg::{AuctionKeyOffset, MinReservePriceOffset, QueryMsg}; use crate::state::{auctions, Auction, Config, HaltManager, HALT_MANAGER}; use crate::state::{CONFIG, MIN_RESERVE_PRICES}; -use cosmwasm_std::{coin, to_binary, Addr, Binary, Coin, Deps, Env, StdResult}; +use cosmwasm_std::{coin, to_json_binary, Addr, Binary, Coin, Deps, Env, StdResult}; use cw_storage_plus::Bound; use sg_marketplace_common::query::{unpack_query_options, QueryOptions}; @@ -16,20 +16,20 @@ use cosmwasm_std::entry_point; #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&query_config(deps)?), - QueryMsg::HaltManager {} => to_binary(&query_halt_manager(deps)?), - QueryMsg::MinReservePrices { query_options } => to_binary(&query_min_reserve_prices( + QueryMsg::Config {} => to_json_binary(&query_config(deps)?), + QueryMsg::HaltManager {} => to_json_binary(&query_halt_manager(deps)?), + QueryMsg::MinReservePrices { query_options } => to_json_binary(&query_min_reserve_prices( deps, query_options.unwrap_or_default(), )?), QueryMsg::Auction { collection, token_id, - } => to_binary(&query_auction(deps, collection, token_id)?), + } => to_json_binary(&query_auction(deps, collection, token_id)?), QueryMsg::AuctionsBySeller { seller, query_options, - } => to_binary(&query_auctions_by_seller( + } => to_json_binary(&query_auctions_by_seller( deps, seller, query_options.unwrap_or_default(), @@ -37,7 +37,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::AuctionsByEndTime { end_time, query_options, - } => to_binary(&query_auctions_by_end_time( + } => to_json_binary(&query_auctions_by_end_time( deps, end_time, query_options.unwrap_or_default(), diff --git a/packages/sg-marketplace-common/Cargo.toml b/packages/sg-marketplace-common/Cargo.toml index dc5d0879..7c73dab3 100644 --- a/packages/sg-marketplace-common/Cargo.toml +++ b/packages/sg-marketplace-common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sg-marketplace-common" -version = "1.2.0" +version = "1.3.0" authors = [ "Shane Vitarana ", "Tasio Victoria ", @@ -39,7 +39,6 @@ cw721-base = { version = "0.18.0", features = ["library"] } sg721-base = { version = "2.1.0", features = ["library"] } sg721 = { version = "2.1.0", features = ["library"] } -sg-std = "2.1.0" sg1 = "2.1.0" stargaze-fair-burn = { version = "1.0.4", features = ["library"] } @@ -49,7 +48,6 @@ thiserror = "1.0.56" [dev-dependencies] cw-multi-test = "0.16.2" -sg-multi-test = "2.1.0" -test-suite = "2.1.0" -base-minter = "2.1.0" -mockall = { workspace = true } +mockall = "0.12.1" +test-suite = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "test-suite" } +base-minter = { git = "https://github.com/public-awesome/launchpad.git", rev = "897d3ab057d381c1654c7246f3972ddd9f9238ce", package = "base-minter" } diff --git a/packages/sg-marketplace-common/src/coin.rs b/packages/sg-marketplace-common/src/coin.rs index e5737046..77df6144 100644 --- a/packages/sg-marketplace-common/src/coin.rs +++ b/packages/sg-marketplace-common/src/coin.rs @@ -1,5 +1,4 @@ -use cosmwasm_std::{Addr, BankMsg, Coin, Decimal, Uint128}; -use sg_std::{Response, SubMsg}; +use cosmwasm_std::{Addr, BankMsg, Coin, Decimal, Response, SubMsg, Uint128}; pub use crate::errors::MarketplaceStdError; diff --git a/packages/sg-marketplace-common/src/constants.rs b/packages/sg-marketplace-common/src/constants.rs new file mode 100644 index 00000000..36130839 --- /dev/null +++ b/packages/sg-marketplace-common/src/constants.rs @@ -0,0 +1 @@ +pub const NATIVE_DENOM: &str = "ustars"; diff --git a/packages/sg-marketplace-common/src/lib.rs b/packages/sg-marketplace-common/src/lib.rs index 57f91a7d..887ff91d 100644 --- a/packages/sg-marketplace-common/src/lib.rs +++ b/packages/sg-marketplace-common/src/lib.rs @@ -10,6 +10,7 @@ pub mod address; pub mod coin; +pub mod constants; mod errors; pub mod nft; pub mod sale; diff --git a/packages/sg-marketplace-common/src/nft.rs b/packages/sg-marketplace-common/src/nft.rs index 8f46fbae..16e51d5a 100644 --- a/packages/sg-marketplace-common/src/nft.rs +++ b/packages/sg-marketplace-common/src/nft.rs @@ -1,12 +1,11 @@ use cosmwasm_std::{ - to_json_binary, Addr, Api, BlockInfo, Empty, MessageInfo, QuerierWrapper, StdError, StdResult, - WasmMsg, + to_json_binary, Addr, Api, BlockInfo, Empty, MessageInfo, QuerierWrapper, Response, StdError, + StdResult, SubMsg, WasmMsg, }; use cw721::{ApprovalResponse, Cw721ExecuteMsg, OwnerOfResponse}; use cw721_base::helpers::Cw721Contract; use sg721::RoyaltyInfo; use sg721_base::msg::{CollectionInfoResponse, QueryMsg as Sg721QueryMsg}; -use sg_std::{Response, SubMsg}; use std::marker::PhantomData; pub use crate::errors::MarketplaceStdError; diff --git a/packages/sg-marketplace-common/src/sale.rs b/packages/sg-marketplace-common/src/sale.rs index b798b054..fcd6b6c3 100644 --- a/packages/sg-marketplace-common/src/sale.rs +++ b/packages/sg-marketplace-common/src/sale.rs @@ -1,39 +1,17 @@ use crate::coin::transfer_coins; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{coin, Addr, Coin, Decimal, Event, StdError}; -use sg_std::Response; -use stargaze_fair_burn::append_fair_burn_msg; -use std::fmt; - -#[cw_serde] -pub enum FeeType { - FairBurn, - Royalty, - Finder, - Seller, -} - -impl fmt::Display for FeeType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - FeeType::FairBurn => write!(f, "fair-burn-fee"), - FeeType::Royalty => write!(f, "royalty-fee"), - FeeType::Finder => write!(f, "finder-fee"), - FeeType::Seller => write!(f, "seller-fee"), - } - } -} +use cosmwasm_std::{coin, Addr, Coin, Decimal, Response, StdError}; pub struct Fee { - pub fee_type: FeeType, + pub label: String, pub recipient: Addr, pub share: Decimal, } #[cw_serde] pub struct Payment { - pub fee_type: FeeType, + pub label: String, pub recipient: Addr, pub funds: Coin, } @@ -55,9 +33,9 @@ impl NftSaleProcessor { } } - pub fn add_fee(&mut self, fee_type: FeeType, share: Decimal, recipient: Addr) { + pub fn add_fee(&mut self, label: String, share: Decimal, recipient: Addr) { self.fees.push(Fee { - fee_type, + label, share, recipient, }); @@ -77,7 +55,7 @@ impl NftSaleProcessor { seller_amount = seller_amount.checked_sub(fee_amount)?; self.payments.push(Payment { - fee_type: fee.fee_type.clone(), + label: fee.label.clone(), recipient: fee.recipient.clone(), funds: fee_coin, }); @@ -85,7 +63,7 @@ impl NftSaleProcessor { if !seller_amount.is_zero() { self.payments.push(Payment { - fee_type: FeeType::Seller, + label: "seller".to_string(), recipient: self.seller_recipient.clone(), funds: coin(seller_amount.u128(), &denom), }); @@ -94,27 +72,13 @@ impl NftSaleProcessor { Ok(()) } - pub fn find_payment(&self, fee_type: FeeType) -> Option<&Payment> { - self.payments.iter().find(|p| p.fee_type == fee_type) + pub fn find_payment(&self, label: String) -> Option<&Payment> { + self.payments.iter().find(|p| p.label == label) } pub fn payout(&self, mut response: Response) -> Response { for payment in self.payments.iter() { - response = match payment.fee_type { - FeeType::FairBurn => append_fair_burn_msg( - &payment.recipient, - vec![payment.funds.clone()], - None, - response, - ), - _ => transfer_coins(vec![payment.funds.clone()], &payment.recipient, response), - }; - - response = response.add_event( - Event::new(payment.fee_type.to_string()) - .add_attribute("coin", payment.funds.to_string()) - .add_attribute("recipient", payment.recipient.to_string()), - ); + response = transfer_coins(vec![payment.funds.clone()], &payment.recipient, response); } response diff --git a/packages/sg-marketplace-common/src/tests/coin.rs b/packages/sg-marketplace-common/src/tests/coin.rs index df6a16ed..090ab04c 100644 --- a/packages/sg-marketplace-common/src/tests/coin.rs +++ b/packages/sg-marketplace-common/src/tests/coin.rs @@ -1,14 +1,14 @@ -use cosmwasm_std::{coin, Addr, Decimal}; -use sg_std::{Response, NATIVE_DENOM}; - use crate::{ coin::{ bps_to_decimal, checked_transfer_coin, checked_transfer_coins, decimal_to_bps, transfer_coin, transfer_coins, }, + constants::NATIVE_DENOM, MarketplaceStdError, }; +use cosmwasm_std::{coin, Addr, Decimal, Response}; + #[test] fn try_transfer_coin() { let recipient = Addr::unchecked("recipient"); diff --git a/packages/sg-marketplace-common/src/tests/nft.rs b/packages/sg-marketplace-common/src/tests/nft.rs index 69d2c29e..b897ba06 100644 --- a/packages/sg-marketplace-common/src/tests/nft.rs +++ b/packages/sg-marketplace-common/src/tests/nft.rs @@ -1,13 +1,12 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, to_json_binary, Addr, Binary, ContractResult, Decimal, Querier, QuerierResult, QuerierWrapper, - StdError, SystemError, SystemResult, Timestamp, + Response, StdError, SystemError, SystemResult, Timestamp, }; use cw721::{Approval, OwnerOfResponse}; -use mockall::*; +use mockall::{mock, predicate}; use sg721::{RoyaltyInfo, RoyaltyInfoResponse}; use sg721_base::msg::CollectionInfoResponse; -use sg_std::Response; use test_suite::common_setup::templates::base_minter_with_sg721; use crate::nft::{ @@ -15,6 +14,14 @@ use crate::nft::{ owner_of, transfer_nft, }; +mock! { + pub QuerierStruct {} + + impl Querier for QuerierStruct { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; + } +} + #[test] fn try_transfer_nft() { let collection = Addr::unchecked("collection"); @@ -29,13 +36,6 @@ fn try_owner_of() { let creator = bmt.accts.creator; let token_id = 1; - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let return_value = SystemResult::Ok(ContractResult::Ok( to_json_binary(&OwnerOfResponse { owner: creator.to_string(), @@ -71,13 +71,6 @@ fn try_only_owner() { let token_id = 1; - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let return_value = SystemResult::Ok(ContractResult::Ok( to_json_binary(&OwnerOfResponse { owner: creator.to_string(), @@ -115,13 +108,6 @@ fn try_has_approval() { let token_id = 1; - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let return_value = SystemResult::Ok(ContractResult::Err( StdError::generic_err("Approval not found").to_string(), )); @@ -160,13 +146,6 @@ fn try_with_owner_approval() { let token_id = 1; - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let return_value = SystemResult::Ok(ContractResult::Ok( to_json_binary(&OwnerOfResponse { owner: creator.to_string(), @@ -241,13 +220,6 @@ fn try_with_owner_approval() { #[test] fn try_only_tradable() { - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let collection = Addr::unchecked("collection"); let env = mock_env(); @@ -357,13 +329,6 @@ fn try_only_tradable() { #[test] fn try_load_collection_royalties() { - mock! { - QuerierStruct {} - impl Querier for QuerierStruct { - fn raw_query(&self, bin_request: &[u8]) -> QuerierResult; - } - } - let return_value = SystemResult::Ok(ContractResult::Ok( to_json_binary(&CollectionInfoResponse { creator: "creator".to_string(), diff --git a/packages/sg-marketplace-common/src/tests/sale.rs b/packages/sg-marketplace-common/src/tests/sale.rs index 154e51cb..4d8f9e83 100644 --- a/packages/sg-marketplace-common/src/tests/sale.rs +++ b/packages/sg-marketplace-common/src/tests/sale.rs @@ -1,11 +1,7 @@ -use cosmwasm_std::{coin, Addr, BankMsg, Decimal, Uint128, WasmMsg}; -use sg721::RoyaltyInfo; -use sg_std::{CosmosMsg, Response, NATIVE_DENOM}; +use crate::{coin::bps_to_decimal, constants::NATIVE_DENOM, sale::NftSaleProcessor}; -use crate::{ - coin::bps_to_decimal, - sale::{FeeType, NftSaleProcessor}, -}; +use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Decimal, Response, Uint128, WasmMsg}; +use sg721::RoyaltyInfo; #[test] fn try_payout_nft_sale_fees() { @@ -23,7 +19,11 @@ fn try_payout_nft_sale_fees() { }; let mut nft_sale_processor = NftSaleProcessor::new(sale_coin.clone(), seller.clone()); - nft_sale_processor.add_fee(FeeType::FairBurn, trading_fee_percent, fair_burn.clone()); + nft_sale_processor.add_fee( + "fair-burn".to_string(), + trading_fee_percent, + fair_burn.clone(), + ); nft_sale_processor.build_payments().unwrap(); let response = nft_sale_processor.payout(Response::new()); @@ -40,14 +40,18 @@ fn try_payout_nft_sale_fees() { } let mut nft_sale_processor = NftSaleProcessor::new(sale_coin, seller); - nft_sale_processor.add_fee(FeeType::FairBurn, trading_fee_percent, fair_burn.clone()); nft_sale_processor.add_fee( - FeeType::Royalty, + "fair-burn".to_string(), + trading_fee_percent, + fair_burn.clone(), + ); + nft_sale_processor.add_fee( + "royalty".to_string(), royalty_info.share, royalty_info.payment_address.clone(), ); nft_sale_processor.add_fee( - FeeType::Finder, + "finder".to_string(), bps_to_decimal(finders_fee_bps), finder.clone(), );