From fbff55a522091b70935bf6020e0445405ba3c5ba Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sat, 17 Jun 2023 21:06:14 -0700 Subject: [PATCH 01/15] Initial commit outlining cw721-sylvia-base contract --- Cargo.lock | 359 +++++++--- contracts/cw721-sylvia-base/Cargo.toml | 19 + contracts/cw721-sylvia-base/src/contract.rs | 693 ++++++++++++++++++++ contracts/cw721-sylvia-base/src/error.rs | 24 + contracts/cw721-sylvia-base/src/lib.rs | 4 + 5 files changed, 1023 insertions(+), 76 deletions(-) create mode 100644 contracts/cw721-sylvia-base/Cargo.toml create mode 100644 contracts/cw721-sylvia-base/src/contract.rs create mode 100644 contracts/cw721-sylvia-base/src/error.rs create mode 100644 contracts/cw721-sylvia-base/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index db676dd54..7164cebd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", @@ -33,9 +33,9 @@ checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" -version = "1.5.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bech32" @@ -54,30 +54,30 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bnum" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cfg-if" @@ -91,6 +91,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "const_panic" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cosmwasm-crypto" version = "1.5.0" @@ -111,7 +126,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" dependencies = [ - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -135,7 +150,7 @@ checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -162,18 +177,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -256,7 +271,7 @@ checksum = "a1d3bf2e0f341bb6cc100d7d441d31cf713fbd3ce0c511f91e79f14b40a889af" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -341,9 +356,9 @@ dependencies = [ [[package]] name = "cw20" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -486,6 +501,23 @@ dependencies = [ "serde", ] +[[package]] +name = "cw721-sylvia-base" +version = "0.1.0" +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", + "schemars", + "serde", + "sylvia", + "thiserror", +] + [[package]] name = "der" version = "0.7.8" @@ -504,7 +536,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -522,7 +554,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.3", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -530,15 +562,15 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.10" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b0705efd4599c15a38151f4721f7bc388306f61084d3bfd50bd07fbca5cb60" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", @@ -555,7 +587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek", - "hashbrown", + "hashbrown 0.12.3", "hex", "rand_core 0.6.4", "serde", @@ -565,15 +597,15 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -588,6 +620,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "ff" version = "0.13.0" @@ -606,9 +644,9 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -617,9 +655,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -646,6 +684,12 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "hex" version = "0.4.3" @@ -661,6 +705,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + [[package]] name = "itertools" version = "0.10.5" @@ -690,15 +744,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "k256" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if", "ecdsa", @@ -708,17 +762,50 @@ dependencies = [ "signature", ] +[[package]] +name = "konst" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d712a8c49d4274f8d8a5cf61368cb5f3c143d149882b1a2918129e53395fdb0" +dependencies = [ + "const_panic", + "konst_kernel", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac6ea8c376b6e208a81cf39b8e82bebf49652454d98a4829e907dac16ef1790" +dependencies = [ + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" + [[package]] name = "libc" -version = "0.2.139" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "once_cell" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -736,11 +823,45 @@ dependencies = [ "spki", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -775,7 +896,7 @@ dependencies = [ "itertools 0.10.5", "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] @@ -788,7 +909,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -827,9 +948,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" @@ -852,14 +973,14 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "sec1" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", @@ -884,11 +1005,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15bee9b04dd165c3f4e142628982ddde884c2022a89e8ddf99c4829bf2c3a58" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ "serde", ] @@ -901,7 +1031,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -912,14 +1042,14 @@ checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -952,9 +1082,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -962,9 +1092,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -978,15 +1108,45 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "sylvia" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "66fc0aae39bc8a76742c1455e3bda09976224e31d236de0e468c91fbc2414c79" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "konst", + "schemars", + "serde", + "serde-cw-value", + "serde-json-wasm", + "sylvia-derive", +] + +[[package]] +name = "sylvia-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c50a057bb2787c04dd93a203809e79d424d5de21b5024e23bc8d1cd5e6f34d4" +dependencies = [ + "convert_case", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -995,9 +1155,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -1021,20 +1181,58 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", ] [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "typewit" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779a69cc5f9a7388274a0a8a353eb1c9e45195f9ae74a26690b055a7cf9592a" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "version_check" @@ -1048,8 +1246,17 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winnow" +version = "0.5.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" +dependencies = [ + "memchr", +] + [[package]] name = "zeroize" -version = "1.5.7" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml new file mode 100644 index 000000000..cdaecfd59 --- /dev/null +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cw721-sylvia-base" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-schema.workspace = true +cosmwasm-std.workspace = true +cw-ownable.workspace = true +cw-storage-plus.workspace = true +cw-utils.workspace = true +cw2.workspace = true +cw721.workspace = true +schemars.workspace = true +serde.workspace = true +sylvia = "0.5.0" +thiserror.workspace = true diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs new file mode 100644 index 000000000..1eed48617 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -0,0 +1,693 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + to_binary, Addr, Binary, BlockInfo, CustomMsg, Deps, Env, Order, Response, StdError, StdResult, +}; +use cw721::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721Query, + Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, + OwnerOfResponse, TokensResponse, +}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_utils::maybe_addr; +use sylvia::types::InstantiateCtx; +use sylvia::{contract, entry_points}; + +#[cw_serde] +pub struct Approval { + /// Account that can transfer/send the token + pub spender: Addr, + /// When the Approval expires (maybe Expiration::never) + pub expires: Expiration, +} + +impl Approval { + pub fn is_expired(&self, block: &BlockInfo) -> bool { + self.expires.is_expired(block) + } +} + +/// Shows who can mint these tokens +#[cw_serde] +pub struct MinterResponse { + pub minter: Option, +} + +#[cw_serde] +pub struct TokenInfo { + /// The owner of the newly minted NFT + pub owner: Addr, + /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much + pub approvals: Vec, + + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, +} + +/// Indexed map for NFT tokens by owner +pub struct TokenIndexes<'a> { + pub owner: MultiIndex<'a, Addr, TokenInfo, String>, +} +impl<'a> IndexList for TokenIndexes<'a> { + fn get_indexes(&'_ self) -> Box + '_)> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} + +pub struct Cw721Contract<'a> { + pub contract_info: Item<'a, ContractInfoResponse>, + pub token_count: Item<'a, u64>, + /// Stored as (granter, operator) giving operator full control over granter's account + pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, + pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>>, +} + +#[cw_serde] +pub struct InstantiateMsgData { + /// Name of the NFT contract + pub name: String, + /// Symbol of the NFT contract + pub symbol: String, + + /// The minter is the only one who can create new NFTs. + /// This is designed for a base NFT that is controlled by an external program + /// or contract. You will likely replace this with custom logic in custom NFTs + pub minter: String, +} + +#[entry_points] +#[contract] +impl Cw721Contract<'_> { + pub fn new() -> Self { + let indexes = TokenIndexes { + owner: MultiIndex::new(token_owner_idx, "tokens", "tokens__owner"), + }; + Self { + contract_info: Item::new("contract_info"), + token_count: Item::new("token_count"), + operators: Map::new("operators"), + tokens: IndexedMap::new("tokens", indexes), + } + } + + #[msg(instantiate)] + pub fn instantiate( + &self, + ctx: InstantiateCtx, + data: InstantiateMsgData, + ) -> StdResult { + let info = ContractInfoResponse { + name: data.name, + symbol: data.symbol, + }; + self.contract_info.save(ctx.deps.storage, &info)?; + + cw_ownable::initialize_owner(ctx.deps.storage, ctx.deps.api, Some(&data.minter))?; + + Ok(Response::new()) + } + + // Execute msgs + // // TODO convert to ExecCtx + // // TODO remove extension + // pub fn mint( + // &self, + // deps: DepsMut, + // info: MessageInfo, + // token_id: String, + // owner: String, + // token_uri: Option, + // extension: T, + // ) -> Result, ContractError> { + // cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // // create the token + // let token = TokenInfo { + // owner: deps.api.addr_validate(&owner)?, + // approvals: vec![], + // token_uri, + // extension, + // }; + // self.tokens + // .update(deps.storage, &token_id, |old| match old { + // Some(_) => Err(ContractError::Claimed {}), + // None => Ok(token), + // })?; + + // self.increment_tokens(deps.storage)?; + + // Ok(Response::new() + // .add_attribute("action", "mint") + // .add_attribute("minter", info.sender) + // .add_attribute("owner", owner) + // .add_attribute("token_id", token_id)) + // } + + // pub fn update_ownership( + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // action: cw_ownable::Action, + // ) -> Result, ContractError> { + // let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + // Ok(Response::new().add_attributes(ownership.into_attributes())) + // } + + // fn transfer_nft( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // recipient: String, + // token_id: String, + // ) -> Result, ContractError> { + // self._transfer_nft(deps, &env, &info, &recipient, &token_id)?; + + // Ok(Response::new() + // .add_attribute("action", "transfer_nft") + // .add_attribute("sender", info.sender) + // .add_attribute("recipient", recipient) + // .add_attribute("token_id", token_id)) + // } + + // fn send_nft( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // contract: String, + // token_id: String, + // msg: Binary, + // ) -> Result, ContractError> { + // // Transfer token + // self._transfer_nft(deps, &env, &info, &contract, &token_id)?; + + // let send = Cw721ReceiveMsg { + // sender: info.sender.to_string(), + // token_id: token_id.clone(), + // msg, + // }; + + // // Send message + // Ok(Response::new() + // .add_message(send.into_cosmos_msg(contract.clone())?) + // .add_attribute("action", "send_nft") + // .add_attribute("sender", info.sender) + // .add_attribute("recipient", contract) + // .add_attribute("token_id", token_id)) + // } + + // fn approve( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // spender: String, + // token_id: String, + // expires: Option, + // ) -> Result, ContractError> { + // self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?; + + // Ok(Response::new() + // .add_attribute("action", "approve") + // .add_attribute("sender", info.sender) + // .add_attribute("spender", spender) + // .add_attribute("token_id", token_id)) + // } + + // fn revoke( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // spender: String, + // token_id: String, + // ) -> Result, ContractError> { + // self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?; + + // Ok(Response::new() + // .add_attribute("action", "revoke") + // .add_attribute("sender", info.sender) + // .add_attribute("spender", spender) + // .add_attribute("token_id", token_id)) + // } + + // fn approve_all( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // operator: String, + // expires: Option, + // ) -> Result, ContractError> { + // // reject expired data as invalid + // let expires = expires.unwrap_or_default(); + // if expires.is_expired(&env.block) { + // return Err(ContractError::Expired {}); + // } + + // // set the operator for us + // let operator_addr = deps.api.addr_validate(&operator)?; + // self.operators + // .save(deps.storage, (&info.sender, &operator_addr), &expires)?; + + // Ok(Response::new() + // .add_attribute("action", "approve_all") + // .add_attribute("sender", info.sender) + // .add_attribute("operator", operator)) + // } + + // fn revoke_all( + // &self, + // deps: DepsMut, + // _env: Env, + // info: MessageInfo, + // operator: String, + // ) -> Result, ContractError> { + // let operator_addr = deps.api.addr_validate(&operator)?; + // self.operators + // .remove(deps.storage, (&info.sender, &operator_addr)); + + // Ok(Response::new() + // .add_attribute("action", "revoke_all") + // .add_attribute("sender", info.sender) + // .add_attribute("operator", operator)) + // } + + // fn burn( + // &self, + // deps: DepsMut, + // env: Env, + // info: MessageInfo, + // token_id: String, + // ) -> Result, ContractError> { + // let token = self.tokens.load(deps.storage, &token_id)?; + // self.check_can_send(deps.as_ref(), &env, &info, &token)?; + + // self.tokens.remove(deps.storage, &token_id)?; + // self.decrement_tokens(deps.storage)?; + + // Ok(Response::new() + // .add_attribute("action", "burn") + // .add_attribute("sender", info.sender) + // .add_attribute("token_id", token_id)) + // } + + // pub fn _transfer_nft( + // &self, + // deps: DepsMut, + // env: &Env, + // info: &MessageInfo, + // recipient: &str, + // token_id: &str, + // ) -> Result, ContractError> { + // let mut token = self.tokens.load(deps.storage, token_id)?; + // // ensure we have permissions + // self.check_can_send(deps.as_ref(), env, info, &token)?; + // // set owner and remove existing approvals + // token.owner = deps.api.addr_validate(recipient)?; + // token.approvals = vec![]; + // self.tokens.save(deps.storage, token_id, &token)?; + // Ok(token) + // } + + // #[allow(clippy::too_many_arguments)] + // pub fn _update_approvals( + // &self, + // deps: DepsMut, + // env: &Env, + // info: &MessageInfo, + // spender: &str, + // token_id: &str, + // // if add == false, remove. if add == true, remove then set with this expiration + // add: bool, + // expires: Option, + // ) -> Result, ContractError> { + // let mut token = self.tokens.load(deps.storage, token_id)?; + // // ensure we have permissions + // self.check_can_approve(deps.as_ref(), env, info, &token)?; + + // // update the approval list (remove any for the same spender before adding) + // let spender_addr = deps.api.addr_validate(spender)?; + // token.approvals.retain(|apr| apr.spender != spender_addr); + + // // only difference between approve and revoke + // if add { + // // reject expired data as invalid + // let expires = expires.unwrap_or_default(); + // if expires.is_expired(&env.block) { + // return Err(ContractError::Expired {}); + // } + // let approval = Approval { + // spender: spender_addr, + // expires, + // }; + // token.approvals.push(approval); + // } + + // self.tokens.save(deps.storage, token_id, &token)?; + + // Ok(token) + // } + + // /// returns true iff the sender can execute approve or reject on the contract + // pub fn check_can_approve( + // &self, + // deps: Deps, + // env: &Env, + // info: &MessageInfo, + // token: &TokenInfo, + // ) -> Result<(), ContractError> { + // // owner can approve + // if token.owner == info.sender { + // return Ok(()); + // } + // // operator can approve + // let op = self + // .operators + // .may_load(deps.storage, (&token.owner, &info.sender))?; + // match op { + // Some(ex) => { + // if ex.is_expired(&env.block) { + // Err(ContractError::Ownership(OwnershipError::NotOwner)) + // } else { + // Ok(()) + // } + // } + // None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + // } + // } + + // /// returns true iff the sender can transfer ownership of the token + // pub fn check_can_send( + // &self, + // deps: Deps, + // env: &Env, + // info: &MessageInfo, + // token: &TokenInfo, + // ) -> Result<(), ContractError> { + // // owner can send + // if token.owner == info.sender { + // return Ok(()); + // } + + // // any non-expired token approval can send + // if token + // .approvals + // .iter() + // .any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block)) + // { + // return Ok(()); + // } + + // // operator can send + // let op = self + // .operators + // .may_load(deps.storage, (&token.owner, &info.sender))?; + // match op { + // Some(ex) => { + // if ex.is_expired(&env.block) { + // Err(ContractError::Ownership(OwnershipError::NotOwner)) + // } else { + // Ok(()) + // } + // } + // None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + // } + // } + + // // QUERIES + // // TODO move me to a separate file + // // TODO use QueryCtx + // // TODO add interfaces to cw721 package + // #[msg(query)] + // pub fn contract_info(&self, deps: Deps) -> StdResult { + // self.contract_info.load(deps.storage) + // } + + // #[msg(query)] + // pub fn num_tokens(&self, deps: Deps) -> StdResult { + // let count = self.token_count(deps.storage)?; + // Ok(NumTokensResponse { count }) + // } + + // #[msg(query)] + // pub fn nft_info(&self, deps: Deps, token_id: String) -> StdResult StdResult { + // let info = self.tokens.load(deps.storage, &token_id)?; + // Ok(OwnerOfResponse { + // owner: info.owner.to_string(), + // approvals: humanize_approvals(&env.block, &info, include_expired), + // }) + // } + + // /// operator returns the approval status of an operator for a given owner if exists + // #[msg(query)] + // pub fn operator( + // &self, + // deps: Deps, + // env: Env, + // owner: String, + // operator: String, + // include_expired: bool, + // ) -> StdResult { + // let owner_addr = deps.api.addr_validate(&owner)?; + // let operator_addr = deps.api.addr_validate(&operator)?; + + // let info = self + // .operators + // .may_load(deps.storage, (&owner_addr, &operator_addr))?; + + // if let Some(expires) = info { + // if !include_expired && expires.is_expired(&env.block) { + // return Err(StdError::not_found("Approval not found")); + // } + + // return Ok(OperatorResponse { + // approval: cw721::Approval { + // spender: operator, + // expires, + // }, + // }); + // } + + // Err(StdError::not_found("Approval not found")) + // } + + // /// operators returns all operators owner given access to + // #[msg(query)] + // pub fn operators( + // &self, + // deps: Deps, + // env: Env, + // owner: String, + // include_expired: bool, + // start_after: Option, + // limit: Option, + // ) -> StdResult { + // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // let start_addr = maybe_addr(deps.api, start_after)?; + // let start = start_addr.as_ref().map(Bound::exclusive); + + // let owner_addr = deps.api.addr_validate(&owner)?; + // let res: StdResult> = self + // .operators + // .prefix(&owner_addr) + // .range(deps.storage, start, None, Order::Ascending) + // .filter(|r| { + // include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block) + // }) + // .take(limit) + // .map(self.parse_approval) + // .collect(); + // Ok(OperatorsResponse { operators: res? }) + // } + + // #[msg(query)] + // pub fn approval( + // &self, + // deps: Deps, + // env: Env, + // token_id: String, + // spender: String, + // include_expired: bool, + // ) -> StdResult { + // let token = self.tokens.load(deps.storage, &token_id)?; + + // // token owner has absolute approval + // if token.owner == spender { + // let approval = cw721::Approval { + // spender: token.owner.to_string(), + // expires: Expiration::Never {}, + // }; + // return Ok(ApprovalResponse { approval }); + // } + + // let filtered: Vec<_> = token + // .approvals + // .into_iter() + // .filter(|t| t.spender == spender) + // .filter(|t| include_expired || !t.is_expired(&env.block)) + // .map(|a| cw721::Approval { + // spender: a.spender.into_string(), + // expires: a.expires, + // }) + // .collect(); + + // if filtered.is_empty() { + // return Err(StdError::not_found("Approval not found")); + // } + // // we expect only one item + // let approval = filtered[0].clone(); + + // Ok(ApprovalResponse { approval }) + // } + + // /// approvals returns all approvals owner given access to + // #[msg(query)] + // pub fn approvals( + // &self, + // deps: Deps, + // env: Env, + // token_id: String, + // include_expired: bool, + // ) -> StdResult { + // let token = self.tokens.load(deps.storage, &token_id)?; + // let approvals: Vec<_> = token + // .approvals + // .into_iter() + // .filter(|t| include_expired || !t.is_expired(&env.block)) + // .map(|a| cw721::Approval { + // spender: a.spender.into_string(), + // expires: a.expires, + // }) + // .collect(); + + // Ok(ApprovalsResponse { approvals }) + // } + + // #[msg(query)] + // pub fn tokens( + // &self, + // deps: Deps, + // owner: String, + // start_after: Option, + // limit: Option, + // ) -> StdResult { + // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + // let owner_addr = deps.api.addr_validate(&owner)?; + // let tokens: Vec = self + // .tokens + // .idx + // .owner + // .prefix(owner_addr) + // .keys(deps.storage, start, None, Order::Ascending) + // .take(limit) + // .collect::>>()?; + + // Ok(TokensResponse { tokens }) + // } + + // #[msg(query)] + // pub fn all_tokens( + // &self, + // deps: Deps, + // start_after: Option, + // limit: Option, + // ) -> StdResult { + // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + // let tokens: StdResult> = self + // .tokens + // .range(deps.storage, start, None, Order::Ascending) + // .take(limit) + // .map(|item| item.map(|(k, _)| k)) + // .collect(); + + // Ok(TokensResponse { tokens: tokens? }) + // } + + // #[msg(query)] + // pub fn all_nft_info( + // &self, + // deps: Deps, + // env: Env, + // token_id: String, + // include_expired: bool, + // ) -> StdResult { + // let info = self.tokens.load(deps.storage, &token_id)?; + // Ok(AllNftInfoResponse { + // access: OwnerOfResponse { + // owner: info.owner.to_string(), + // approvals: self.humanize_approvals(&env.block, &info, include_expired), + // }, + // info: NftInfoResponse { + // token_uri: info.token_uri, + // extension: info.extension, + // }, + // }) + // } + + // #[msg(query)] + // pub fn minter(&self, deps: Deps) -> StdResult { + // let minter = cw_ownable::get_ownership(deps.storage)? + // .owner + // .map(|a| a.into_string()); + + // Ok(MinterResponse { minter }) + // } + + // #[msg(query)] + // pub fn ownership(deps: Deps) -> StdResult> { + // cw_ownable::get_ownership(deps.storage) + // } + + // fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + // item.map(|(spender, expires)| cw721::Approval { + // spender: spender.to_string(), + // expires, + // }) + // } + + // fn humanize_approvals( + // block: &BlockInfo, + // info: &TokenInfo, + // include_expired: bool, + // ) -> Vec { + // info.approvals + // .iter() + // .filter(|apr| include_expired || !apr.is_expired(block)) + // .map(humanize_approval) + // .collect() + // } + + // fn humanize_approval(approval: &Approval) -> cw721::Approval { + // cw721::Approval { + // spender: approval.spender.to_string(), + // expires: approval.expires, + // } + // } +} + +pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { + d.owner.clone() +} diff --git a/contracts/cw721-sylvia-base/src/error.rs b/contracts/cw721-sylvia-base/src/error.rs new file mode 100644 index 000000000..99d5a842b --- /dev/null +++ b/contracts/cw721-sylvia-base/src/error.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::StdError; +use cw_ownable::OwnershipError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Ownership(#[from] OwnershipError), + + #[error(transparent)] + Version(#[from] cw2::VersionError), + + #[error("token_id already claimed")] + Claimed {}, + + #[error("Cannot set approval that is already expired")] + Expired {}, + + #[error("Approval not found for: {spender}")] + ApprovalNotFound { spender: String }, +} diff --git a/contracts/cw721-sylvia-base/src/lib.rs b/contracts/cw721-sylvia-base/src/lib.rs new file mode 100644 index 000000000..226ea88b1 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/lib.rs @@ -0,0 +1,4 @@ +pub mod contract; +mod error; + +pub use crate::error::ContractError; From 9d7cabfbf009a226218a144de189348fbdc43239 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 19 Jun 2023 18:15:08 -0700 Subject: [PATCH 02/15] Basic cw721-sylvia-base contract, add cw721_interface to cw721 package --- Cargo.lock | 1 + Cargo.toml | 1 + contracts/cw721-sylvia-base/Cargo.toml | 28 +- contracts/cw721-sylvia-base/src/contract.rs | 1160 +++++++++---------- packages/cw721/Cargo.toml | 1 + packages/cw721/src/cw721_interface.rs | 141 +++ packages/cw721/src/lib.rs | 1 + 7 files changed, 734 insertions(+), 599 deletions(-) create mode 100644 packages/cw721/src/cw721_interface.rs diff --git a/Cargo.lock b/Cargo.lock index 7164cebd8..d6b426b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,7 @@ dependencies = [ "cw-utils 1.0.3", "schemars", "serde", + "sylvia", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6a8683410..4ddcf163b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ cw-storage-plus = "1.1.0" cw-utils = "1.0.1" schemars = "0.8.11" serde = { version = "1.0.152", default-features = false, features = ["derive"] } +sylvia = "0.5.0" thiserror = "1.0.38" [profile.release.package.cw721-base] diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml index cdaecfd59..b1af0b0bc 100644 --- a/contracts/cw721-sylvia-base/Cargo.toml +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "cw721-sylvia-base" version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } [dependencies] -cosmwasm-schema.workspace = true -cosmwasm-std.workspace = true -cw-ownable.workspace = true -cw-storage-plus.workspace = true -cw-utils.workspace = true -cw2.workspace = true -cw721.workspace = true -schemars.workspace = true -serde.workspace = true -sylvia = "0.5.0" -thiserror.workspace = true +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +sylvia = { workspace = true } +thiserror = { workspace = true } diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 1eed48617..70cb8b334 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -1,17 +1,21 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ - to_binary, Addr, Binary, BlockInfo, CustomMsg, Deps, Env, Order, Response, StdError, StdResult, -}; +use cosmwasm_std::{Addr, Binary, BlockInfo, Empty, Order, Response, StdError, StdResult, Storage}; use cw721::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721Query, - Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, - OwnerOfResponse, TokensResponse, + cw721_interface, AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + Cw721ReceiveMsg, Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, + OperatorsResponse, OwnerOfResponse, TokensResponse, }; +use cw_ownable::{Ownership, OwnershipError}; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use cw_utils::maybe_addr; -use sylvia::types::InstantiateCtx; +use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use sylvia::{contract, entry_points}; +use crate::ContractError; + +const DEFAULT_LIMIT: u32 = 10; +const MAX_LIMIT: u32 = 100; + #[cw_serde] pub struct Approval { /// Account that can transfer/send the token @@ -43,6 +47,8 @@ pub struct TokenInfo { /// Should point to a JSON file that conforms to the ERC721 /// Metadata JSON Schema pub token_uri: Option, + + pub extension: Empty, } /// Indexed map for NFT tokens by owner @@ -77,8 +83,10 @@ pub struct InstantiateMsgData { pub minter: String, } -#[entry_points] +#[cfg_attr(not(feature = "library"), entry_points)] #[contract] +#[error(ContractError)] +#[messages(cw721_interface as Cw721Interface)] impl Cw721Contract<'_> { pub fn new() -> Self { let indexes = TokenIndexes { @@ -98,6 +106,8 @@ impl Cw721Contract<'_> { ctx: InstantiateCtx, data: InstantiateMsgData, ) -> StdResult { + // TODO save contract metadata???? + let info = ContractInfoResponse { name: data.name, symbol: data.symbol, @@ -109,585 +119,565 @@ impl Cw721Contract<'_> { Ok(Response::new()) } - // Execute msgs - // // TODO convert to ExecCtx - // // TODO remove extension - // pub fn mint( - // &self, - // deps: DepsMut, - // info: MessageInfo, - // token_id: String, - // owner: String, - // token_uri: Option, - // extension: T, - // ) -> Result, ContractError> { - // cw_ownable::assert_owner(deps.storage, &info.sender)?; - - // // create the token - // let token = TokenInfo { - // owner: deps.api.addr_validate(&owner)?, - // approvals: vec![], - // token_uri, - // extension, - // }; - // self.tokens - // .update(deps.storage, &token_id, |old| match old { - // Some(_) => Err(ContractError::Claimed {}), - // None => Ok(token), - // })?; - - // self.increment_tokens(deps.storage)?; - - // Ok(Response::new() - // .add_attribute("action", "mint") - // .add_attribute("minter", info.sender) - // .add_attribute("owner", owner) - // .add_attribute("token_id", token_id)) - // } - - // pub fn update_ownership( - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // action: cw_ownable::Action, - // ) -> Result, ContractError> { - // let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; - // Ok(Response::new().add_attributes(ownership.into_attributes())) - // } - - // fn transfer_nft( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // recipient: String, - // token_id: String, - // ) -> Result, ContractError> { - // self._transfer_nft(deps, &env, &info, &recipient, &token_id)?; - - // Ok(Response::new() - // .add_attribute("action", "transfer_nft") - // .add_attribute("sender", info.sender) - // .add_attribute("recipient", recipient) - // .add_attribute("token_id", token_id)) - // } - - // fn send_nft( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // contract: String, - // token_id: String, - // msg: Binary, - // ) -> Result, ContractError> { - // // Transfer token - // self._transfer_nft(deps, &env, &info, &contract, &token_id)?; - - // let send = Cw721ReceiveMsg { - // sender: info.sender.to_string(), - // token_id: token_id.clone(), - // msg, - // }; - - // // Send message - // Ok(Response::new() - // .add_message(send.into_cosmos_msg(contract.clone())?) - // .add_attribute("action", "send_nft") - // .add_attribute("sender", info.sender) - // .add_attribute("recipient", contract) - // .add_attribute("token_id", token_id)) - // } - - // fn approve( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // spender: String, - // token_id: String, - // expires: Option, - // ) -> Result, ContractError> { - // self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?; - - // Ok(Response::new() - // .add_attribute("action", "approve") - // .add_attribute("sender", info.sender) - // .add_attribute("spender", spender) - // .add_attribute("token_id", token_id)) - // } - - // fn revoke( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // spender: String, - // token_id: String, - // ) -> Result, ContractError> { - // self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?; - - // Ok(Response::new() - // .add_attribute("action", "revoke") - // .add_attribute("sender", info.sender) - // .add_attribute("spender", spender) - // .add_attribute("token_id", token_id)) - // } - - // fn approve_all( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // operator: String, - // expires: Option, - // ) -> Result, ContractError> { - // // reject expired data as invalid - // let expires = expires.unwrap_or_default(); - // if expires.is_expired(&env.block) { - // return Err(ContractError::Expired {}); - // } - - // // set the operator for us - // let operator_addr = deps.api.addr_validate(&operator)?; - // self.operators - // .save(deps.storage, (&info.sender, &operator_addr), &expires)?; - - // Ok(Response::new() - // .add_attribute("action", "approve_all") - // .add_attribute("sender", info.sender) - // .add_attribute("operator", operator)) - // } - - // fn revoke_all( - // &self, - // deps: DepsMut, - // _env: Env, - // info: MessageInfo, - // operator: String, - // ) -> Result, ContractError> { - // let operator_addr = deps.api.addr_validate(&operator)?; - // self.operators - // .remove(deps.storage, (&info.sender, &operator_addr)); - - // Ok(Response::new() - // .add_attribute("action", "revoke_all") - // .add_attribute("sender", info.sender) - // .add_attribute("operator", operator)) - // } - - // fn burn( - // &self, - // deps: DepsMut, - // env: Env, - // info: MessageInfo, - // token_id: String, - // ) -> Result, ContractError> { - // let token = self.tokens.load(deps.storage, &token_id)?; - // self.check_can_send(deps.as_ref(), &env, &info, &token)?; - - // self.tokens.remove(deps.storage, &token_id)?; - // self.decrement_tokens(deps.storage)?; - - // Ok(Response::new() - // .add_attribute("action", "burn") - // .add_attribute("sender", info.sender) - // .add_attribute("token_id", token_id)) - // } - - // pub fn _transfer_nft( - // &self, - // deps: DepsMut, - // env: &Env, - // info: &MessageInfo, - // recipient: &str, - // token_id: &str, - // ) -> Result, ContractError> { - // let mut token = self.tokens.load(deps.storage, token_id)?; - // // ensure we have permissions - // self.check_can_send(deps.as_ref(), env, info, &token)?; - // // set owner and remove existing approvals - // token.owner = deps.api.addr_validate(recipient)?; - // token.approvals = vec![]; - // self.tokens.save(deps.storage, token_id, &token)?; - // Ok(token) - // } - - // #[allow(clippy::too_many_arguments)] - // pub fn _update_approvals( - // &self, - // deps: DepsMut, - // env: &Env, - // info: &MessageInfo, - // spender: &str, - // token_id: &str, - // // if add == false, remove. if add == true, remove then set with this expiration - // add: bool, - // expires: Option, - // ) -> Result, ContractError> { - // let mut token = self.tokens.load(deps.storage, token_id)?; - // // ensure we have permissions - // self.check_can_approve(deps.as_ref(), env, info, &token)?; - - // // update the approval list (remove any for the same spender before adding) - // let spender_addr = deps.api.addr_validate(spender)?; - // token.approvals.retain(|apr| apr.spender != spender_addr); - - // // only difference between approve and revoke - // if add { - // // reject expired data as invalid - // let expires = expires.unwrap_or_default(); - // if expires.is_expired(&env.block) { - // return Err(ContractError::Expired {}); - // } - // let approval = Approval { - // spender: spender_addr, - // expires, - // }; - // token.approvals.push(approval); - // } - - // self.tokens.save(deps.storage, token_id, &token)?; - - // Ok(token) - // } - - // /// returns true iff the sender can execute approve or reject on the contract - // pub fn check_can_approve( - // &self, - // deps: Deps, - // env: &Env, - // info: &MessageInfo, - // token: &TokenInfo, - // ) -> Result<(), ContractError> { - // // owner can approve - // if token.owner == info.sender { - // return Ok(()); - // } - // // operator can approve - // let op = self - // .operators - // .may_load(deps.storage, (&token.owner, &info.sender))?; - // match op { - // Some(ex) => { - // if ex.is_expired(&env.block) { - // Err(ContractError::Ownership(OwnershipError::NotOwner)) - // } else { - // Ok(()) - // } - // } - // None => Err(ContractError::Ownership(OwnershipError::NotOwner)), - // } - // } - - // /// returns true iff the sender can transfer ownership of the token - // pub fn check_can_send( - // &self, - // deps: Deps, - // env: &Env, - // info: &MessageInfo, - // token: &TokenInfo, - // ) -> Result<(), ContractError> { - // // owner can send - // if token.owner == info.sender { - // return Ok(()); - // } - - // // any non-expired token approval can send - // if token - // .approvals - // .iter() - // .any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block)) - // { - // return Ok(()); - // } - - // // operator can send - // let op = self - // .operators - // .may_load(deps.storage, (&token.owner, &info.sender))?; - // match op { - // Some(ex) => { - // if ex.is_expired(&env.block) { - // Err(ContractError::Ownership(OwnershipError::NotOwner)) - // } else { - // Ok(()) - // } - // } - // None => Err(ContractError::Ownership(OwnershipError::NotOwner)), - // } - // } - - // // QUERIES - // // TODO move me to a separate file - // // TODO use QueryCtx - // // TODO add interfaces to cw721 package - // #[msg(query)] - // pub fn contract_info(&self, deps: Deps) -> StdResult { - // self.contract_info.load(deps.storage) - // } - - // #[msg(query)] - // pub fn num_tokens(&self, deps: Deps) -> StdResult { - // let count = self.token_count(deps.storage)?; - // Ok(NumTokensResponse { count }) - // } - - // #[msg(query)] - // pub fn nft_info(&self, deps: Deps, token_id: String) -> StdResult StdResult { - // let info = self.tokens.load(deps.storage, &token_id)?; - // Ok(OwnerOfResponse { - // owner: info.owner.to_string(), - // approvals: humanize_approvals(&env.block, &info, include_expired), - // }) - // } - - // /// operator returns the approval status of an operator for a given owner if exists - // #[msg(query)] - // pub fn operator( - // &self, - // deps: Deps, - // env: Env, - // owner: String, - // operator: String, - // include_expired: bool, - // ) -> StdResult { - // let owner_addr = deps.api.addr_validate(&owner)?; - // let operator_addr = deps.api.addr_validate(&operator)?; - - // let info = self - // .operators - // .may_load(deps.storage, (&owner_addr, &operator_addr))?; - - // if let Some(expires) = info { - // if !include_expired && expires.is_expired(&env.block) { - // return Err(StdError::not_found("Approval not found")); - // } - - // return Ok(OperatorResponse { - // approval: cw721::Approval { - // spender: operator, - // expires, - // }, - // }); - // } - - // Err(StdError::not_found("Approval not found")) - // } - - // /// operators returns all operators owner given access to - // #[msg(query)] - // pub fn operators( - // &self, - // deps: Deps, - // env: Env, - // owner: String, - // include_expired: bool, - // start_after: Option, - // limit: Option, - // ) -> StdResult { - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - // let start_addr = maybe_addr(deps.api, start_after)?; - // let start = start_addr.as_ref().map(Bound::exclusive); - - // let owner_addr = deps.api.addr_validate(&owner)?; - // let res: StdResult> = self - // .operators - // .prefix(&owner_addr) - // .range(deps.storage, start, None, Order::Ascending) - // .filter(|r| { - // include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block) - // }) - // .take(limit) - // .map(self.parse_approval) - // .collect(); - // Ok(OperatorsResponse { operators: res? }) - // } - - // #[msg(query)] - // pub fn approval( - // &self, - // deps: Deps, - // env: Env, - // token_id: String, - // spender: String, - // include_expired: bool, - // ) -> StdResult { - // let token = self.tokens.load(deps.storage, &token_id)?; - - // // token owner has absolute approval - // if token.owner == spender { - // let approval = cw721::Approval { - // spender: token.owner.to_string(), - // expires: Expiration::Never {}, - // }; - // return Ok(ApprovalResponse { approval }); - // } - - // let filtered: Vec<_> = token - // .approvals - // .into_iter() - // .filter(|t| t.spender == spender) - // .filter(|t| include_expired || !t.is_expired(&env.block)) - // .map(|a| cw721::Approval { - // spender: a.spender.into_string(), - // expires: a.expires, - // }) - // .collect(); - - // if filtered.is_empty() { - // return Err(StdError::not_found("Approval not found")); - // } - // // we expect only one item - // let approval = filtered[0].clone(); - - // Ok(ApprovalResponse { approval }) - // } - - // /// approvals returns all approvals owner given access to - // #[msg(query)] - // pub fn approvals( - // &self, - // deps: Deps, - // env: Env, - // token_id: String, - // include_expired: bool, - // ) -> StdResult { - // let token = self.tokens.load(deps.storage, &token_id)?; - // let approvals: Vec<_> = token - // .approvals - // .into_iter() - // .filter(|t| include_expired || !t.is_expired(&env.block)) - // .map(|a| cw721::Approval { - // spender: a.spender.into_string(), - // expires: a.expires, - // }) - // .collect(); - - // Ok(ApprovalsResponse { approvals }) - // } - - // #[msg(query)] - // pub fn tokens( - // &self, - // deps: Deps, - // owner: String, - // start_after: Option, - // limit: Option, - // ) -> StdResult { - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - // let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - // let owner_addr = deps.api.addr_validate(&owner)?; - // let tokens: Vec = self - // .tokens - // .idx - // .owner - // .prefix(owner_addr) - // .keys(deps.storage, start, None, Order::Ascending) - // .take(limit) - // .collect::>>()?; - - // Ok(TokensResponse { tokens }) - // } - - // #[msg(query)] - // pub fn all_tokens( - // &self, - // deps: Deps, - // start_after: Option, - // limit: Option, - // ) -> StdResult { - // let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - // let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - // let tokens: StdResult> = self - // .tokens - // .range(deps.storage, start, None, Order::Ascending) - // .take(limit) - // .map(|item| item.map(|(k, _)| k)) - // .collect(); - - // Ok(TokensResponse { tokens: tokens? }) - // } - - // #[msg(query)] - // pub fn all_nft_info( - // &self, - // deps: Deps, - // env: Env, - // token_id: String, - // include_expired: bool, - // ) -> StdResult { - // let info = self.tokens.load(deps.storage, &token_id)?; - // Ok(AllNftInfoResponse { - // access: OwnerOfResponse { - // owner: info.owner.to_string(), - // approvals: self.humanize_approvals(&env.block, &info, include_expired), - // }, - // info: NftInfoResponse { - // token_uri: info.token_uri, - // extension: info.extension, - // }, - // }) - // } - - // #[msg(query)] - // pub fn minter(&self, deps: Deps) -> StdResult { - // let minter = cw_ownable::get_ownership(deps.storage)? - // .owner - // .map(|a| a.into_string()); - - // Ok(MinterResponse { minter }) - // } - - // #[msg(query)] - // pub fn ownership(deps: Deps) -> StdResult> { - // cw_ownable::get_ownership(deps.storage) - // } - - // fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { - // item.map(|(spender, expires)| cw721::Approval { - // spender: spender.to_string(), - // expires, - // }) - // } - - // fn humanize_approvals( - // block: &BlockInfo, - // info: &TokenInfo, - // include_expired: bool, - // ) -> Vec { - // info.approvals - // .iter() - // .filter(|apr| include_expired || !apr.is_expired(block)) - // .map(humanize_approval) - // .collect() - // } - - // fn humanize_approval(approval: &Approval) -> cw721::Approval { - // cw721::Approval { - // spender: approval.spender.to_string(), - // expires: approval.expires, - // } - // } + #[msg(exec)] + pub fn mint( + &self, + ctx: ExecCtx, + token_id: String, + owner: String, + token_uri: Option, + ) -> Result { + cw_ownable::assert_owner(ctx.deps.storage, &ctx.info.sender)?; + + // create the token + let token = TokenInfo { + owner: ctx.deps.api.addr_validate(&owner)?, + approvals: vec![], + token_uri, + extension: Empty {}, + }; + self.tokens + .update(ctx.deps.storage, &token_id, |old| match old { + Some(_) => Err(ContractError::Claimed {}), + None => Ok(token), + })?; + + self.increment_tokens(ctx.deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "mint") + .add_attribute("minter", ctx.info.sender) + .add_attribute("owner", owner) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + pub fn update_ownership( + &self, + ctx: ExecCtx, + action: cw_ownable::Action, + ) -> Result { + let ownership = + cw_ownable::update_ownership(ctx.deps, &ctx.env.block, &ctx.info.sender, action)?; + Ok(Response::new().add_attributes(ownership.into_attributes())) + } + + #[msg(query)] + pub fn minter(&self, ctx: QueryCtx) -> StdResult { + let minter = cw_ownable::get_ownership(ctx.deps.storage)? + .owner + .map(|a| a.into_string()); + + Ok(MinterResponse { minter }) + } + + #[msg(query)] + pub fn ownership(&self, ctx: QueryCtx) -> StdResult> { + cw_ownable::get_ownership(ctx.deps.storage) + } + + pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count.may_load(storage)?.unwrap_or_default() + 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count.may_load(storage)?.unwrap_or_default() - 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn _transfer_nft( + &self, + ctx: &mut ExecCtx, + recipient: &str, + token_id: &str, + ) -> Result { + let mut token = self.tokens.load(ctx.deps.storage, token_id)?; + // ensure we have permissions + self.check_can_send(&ctx, &token)?; + // set owner and remove existing approvals + token.owner = ctx.deps.api.addr_validate(recipient)?; + token.approvals = vec![]; + self.tokens.save(ctx.deps.storage, token_id, &token)?; + Ok(token) + } + + #[allow(clippy::too_many_arguments)] + pub fn _update_approvals( + &self, + ctx: &mut ExecCtx, + spender: &str, + token_id: &str, + // if add == false, remove. if add == true, remove then set with this expiration + add: bool, + expires: Option, + ) -> Result { + let mut token = self.tokens.load(ctx.deps.storage, token_id)?; + // ensure we have permissions + self.check_can_approve(&ctx, &token)?; + + // update the approval list (remove any for the same spender before adding) + let spender_addr = ctx.deps.api.addr_validate(spender)?; + token.approvals.retain(|apr| apr.spender != spender_addr); + + // only difference between approve and revoke + if add { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&ctx.env.block) { + return Err(ContractError::Expired {}); + } + let approval = Approval { + spender: spender_addr, + expires, + }; + token.approvals.push(approval); + } + + self.tokens.save(ctx.deps.storage, token_id, &token)?; + + Ok(token) + } + + /// returns true iff the sender can execute approve or reject on the contract + pub fn check_can_approve(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { + // owner can approve + if token.owner == ctx.info.sender { + return Ok(()); + } + // operator can approve + let op = self + .operators + .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&ctx.env.block) { + Err(ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + } + } + + /// returns true iff the sender can transfer ownership of the token + pub fn check_can_send(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { + // owner can send + if token.owner == ctx.info.sender { + return Ok(()); + } + + // any non-expired token approval can send + if token + .approvals + .iter() + .any(|apr| apr.spender == ctx.info.sender && !apr.is_expired(&ctx.env.block)) + { + return Ok(()); + } + + // operator can send + let op = self + .operators + .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&ctx.env.block) { + Err(ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + } + } +} + +#[contract] +#[messages(cw721_interface as Cw721Interface)] +impl cw721_interface::Cw721Interface for Cw721Contract<'_> { + type Error = ContractError; + + #[msg(exec)] + fn transfer_nft( + &self, + mut ctx: ExecCtx, + recipient: String, + token_id: String, + ) -> Result { + self._transfer_nft(&mut ctx, &recipient, &token_id)?; + + Ok(Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", ctx.info.sender) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn send_nft( + &self, + mut ctx: ExecCtx, + contract: String, + token_id: String, + msg: Binary, + ) -> Result { + // Transfer token + self._transfer_nft(&mut ctx, &contract, &token_id)?; + + let send = Cw721ReceiveMsg { + sender: ctx.info.sender.to_string(), + token_id: token_id.clone(), + msg, + }; + + // Send message + Ok(Response::new() + .add_message(send.into_cosmos_msg(contract.clone())?) + .add_attribute("action", "send_nft") + .add_attribute("sender", ctx.info.sender) + .add_attribute("recipient", contract) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn approve( + &self, + mut ctx: ExecCtx, + spender: String, + token_id: String, + expires: Option, + ) -> Result { + self._update_approvals(&mut ctx, &spender, &token_id, true, expires)?; + + Ok(Response::new() + .add_attribute("action", "approve") + .add_attribute("sender", ctx.info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn revoke( + &self, + mut ctx: ExecCtx, + spender: String, + token_id: String, + ) -> Result { + self._update_approvals(&mut ctx, &spender, &token_id, false, None)?; + + Ok(Response::new() + .add_attribute("action", "revoke") + .add_attribute("sender", ctx.info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn approve_all( + &self, + ctx: ExecCtx, + operator: String, + expires: Option, + ) -> Result { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&ctx.env.block) { + return Err(ContractError::Expired {}); + } + + // set the operator for us + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + self.operators.save( + ctx.deps.storage, + (&ctx.info.sender, &operator_addr), + &expires, + )?; + + Ok(Response::new() + .add_attribute("action", "approve_all") + .add_attribute("sender", ctx.info.sender) + .add_attribute("operator", operator)) + } + + #[msg(exec)] + fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result { + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + self.operators + .remove(ctx.deps.storage, (&ctx.info.sender, &operator_addr)); + + Ok(Response::new() + .add_attribute("action", "revoke_all") + .add_attribute("sender", ctx.info.sender) + .add_attribute("operator", operator)) + } + + #[msg(exec)] + fn burn(&self, ctx: ExecCtx, token_id: String) -> Result { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + self.check_can_send(&ctx, &token)?; + + self.tokens.remove(ctx.deps.storage, &token_id)?; + self.decrement_tokens(ctx.deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", ctx.info.sender) + .add_attribute("token_id", token_id)) + } + + #[msg(query)] + fn contract_info(&self, ctx: QueryCtx) -> StdResult { + self.contract_info.load(ctx.deps.storage) + } + + #[msg(query)] + fn num_tokens(&self, ctx: QueryCtx) -> StdResult { + let count = self + .token_count + .may_load(ctx.deps.storage)? + .unwrap_or_default(); + Ok(NumTokensResponse { count }) + } + + #[msg(query)] + fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult> { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(NftInfoResponse { + token_uri: info.token_uri, + extension: info.extension, + }) + } + + #[msg(query)] + fn owner_of( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&ctx.env.block, &info, include_expired), + }) + } + + /// operator returns the approval status of an operator for a given owner if exists + #[msg(query)] + fn operator( + &self, + ctx: QueryCtx, + owner: String, + operator: String, + include_expired: bool, + ) -> StdResult { + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + + let info = self + .operators + .may_load(ctx.deps.storage, (&owner_addr, &operator_addr))?; + + if let Some(expires) = info { + if !include_expired && expires.is_expired(&ctx.env.block) { + return Err(StdError::not_found("Approval not found")); + } + + return Ok(OperatorResponse { + approval: cw721::Approval { + spender: operator, + expires, + }, + }); + } + + Err(StdError::not_found("Approval not found")) + } + + /// operators returns all operators owner given access to + #[msg(query)] + fn operators( + &self, + ctx: QueryCtx, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_addr = maybe_addr(ctx.deps.api, start_after)?; + let start = start_addr.as_ref().map(Bound::exclusive); + + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let res: StdResult> = self + .operators + .prefix(&owner_addr) + .range(ctx.deps.storage, start, None, Order::Ascending) + .filter(|r| { + include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&ctx.env.block) + }) + .take(limit) + .map(parse_approval) + .collect(); + Ok(OperatorsResponse { operators: res? }) + } + + #[msg(query)] + fn approval( + &self, + ctx: QueryCtx, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + + // token owner has absolute approval + if token.owner == spender { + let approval = cw721::Approval { + spender: token.owner.to_string(), + expires: Expiration::Never {}, + }; + return Ok(ApprovalResponse { approval }); + } + + let filtered: Vec<_> = token + .approvals + .into_iter() + .filter(|t| t.spender == spender) + .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + if filtered.is_empty() { + return Err(StdError::not_found("Approval not found")); + } + // we expect only one item + let approval = filtered[0].clone(); + + Ok(ApprovalResponse { approval }) + } + + /// approvals returns all approvals owner given access to + #[msg(query)] + fn approvals( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + let approvals: Vec<_> = token + .approvals + .into_iter() + .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + Ok(ApprovalsResponse { approvals }) + } + + #[msg(query)] + fn tokens( + &self, + ctx: QueryCtx, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let tokens: Vec = self + .tokens + .idx + .owner + .prefix(owner_addr) + .keys(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + Ok(TokensResponse { tokens }) + } + + #[msg(query)] + fn all_tokens( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + let tokens: StdResult> = self + .tokens + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(k, _)| k)) + .collect(); + + Ok(TokensResponse { tokens: tokens? }) + } + + #[msg(query)] + fn all_nft_info( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult> { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(AllNftInfoResponse:: { + access: OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&ctx.env.block, &info, include_expired), + }, + info: NftInfoResponse { + token_uri: info.token_uri, + extension: Empty {}, + }, + }) + } } pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { d.owner.clone() } + +pub fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + item.map(|(spender, expires)| cw721::Approval { + spender: spender.to_string(), + expires, + }) +} + +pub fn humanize_approvals( + block: &BlockInfo, + info: &TokenInfo, + include_expired: bool, +) -> Vec { + info.approvals + .iter() + .filter(|apr| include_expired || !apr.is_expired(block)) + .map(humanize_approval) + .collect() +} + +pub fn humanize_approval(approval: &Approval) -> cw721::Approval { + cw721::Approval { + spender: approval.spender.to_string(), + expires: approval.expires, + } +} diff --git a/packages/cw721/Cargo.toml b/packages/cw721/Cargo.toml index 0f5967cff..9bea5de3f 100644 --- a/packages/cw721/Cargo.toml +++ b/packages/cw721/Cargo.toml @@ -17,4 +17,5 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-utils = { workspace = true } schemars = { workspace = true } +sylvia = { workspace = true } serde = { workspace = true } diff --git a/packages/cw721/src/cw721_interface.rs b/packages/cw721/src/cw721_interface.rs new file mode 100644 index 000000000..7c8ab64c3 --- /dev/null +++ b/packages/cw721/src/cw721_interface.rs @@ -0,0 +1,141 @@ +use cosmwasm_std::{Binary, Empty, Response, StdResult}; +use cw_utils::Expiration; +use sylvia::cw_std::StdError; +use sylvia::interface; +use sylvia::types::{ExecCtx, QueryCtx}; + +use crate::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, + NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +}; + +#[interface] +pub trait Cw721Interface { + type Error: From; + + #[msg(exec)] + fn transfer_nft( + &self, + ctx: ExecCtx, + recipient: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn send_nft( + &self, + ctx: ExecCtx, + contract: String, + token_id: String, + msg: Binary, + ) -> Result; + + #[msg(exec)] + fn approve( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn approve_all( + &self, + ctx: ExecCtx, + operator: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result; + + #[msg(exec)] + fn burn(&self, ctx: ExecCtx, token_id: String) -> Result; + + #[msg(query)] + fn contract_info(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn num_tokens(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult>; + + #[msg(query)] + fn owner_of( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operator( + &self, + ctx: QueryCtx, + owner: String, + operator: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operators( + &self, + ctx: QueryCtx, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn approval( + &self, + ctx: QueryCtx, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn approvals( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn tokens( + &self, + ctx: QueryCtx, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_tokens( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_nft_info( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult>; +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 312189b64..6ff22bb6e 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cw721_interface; mod msg; mod query; mod receiver; From ceefeb19836299384715df72b824feadef75ebc2 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 19 Jun 2023 19:02:08 -0700 Subject: [PATCH 03/15] Setup multitest with sylvia, basic instantiation test --- Cargo.lock | 5 +++ Cargo.toml | 1 + contracts/cw721-sylvia-base/Cargo.toml | 11 +++++++ contracts/cw721-sylvia-base/src/contract.rs | 7 +++-- contracts/cw721-sylvia-base/src/lib.rs | 3 ++ contracts/cw721-sylvia-base/src/multitest.rs | 33 ++++++++++++++++++++ 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 contracts/cw721-sylvia-base/src/multitest.rs diff --git a/Cargo.lock b/Cargo.lock index d6b426b0c..8564cc052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,8 +506,10 @@ dependencies = [ name = "cw721-sylvia-base" version = "0.1.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -1119,8 +1121,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fc0aae39bc8a76742c1455e3bda09976224e31d236de0e468c91fbc2414c79" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test", + "derivative", "konst", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 4ddcf163b..039bca76f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ documentation = "https://docs.cosmwasm.com" rust-version = "1.65" [workspace.dependencies] +anyhow = "1.0.68" cosmwasm-schema = "1.2.1" cosmwasm-std = "1.2.1" cw2 = "1.1.0" diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml index b1af0b0bc..b37a3b2fa 100644 --- a/contracts/cw721-sylvia-base/Cargo.toml +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -5,6 +5,12 @@ edition = { workspace = true } repository = { workspace = true } license = { workspace = true } +[features] +library = [] +# enables generation of multi-test utilities for sylvia +mt = ["library", "sylvia/mt"] + + [dependencies] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } @@ -17,3 +23,8 @@ schemars = { workspace = true } serde = { workspace = true } sylvia = { workspace = true } thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +sylvia = { workspace = true, features = ["mt"] } diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 70cb8b334..3305528df 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -5,11 +5,12 @@ use cw721::{ Cw721ReceiveMsg, Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, }; +use cw721_interface::Cw721Interface; use cw_ownable::{Ownership, OwnershipError}; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use cw_utils::maybe_addr; +use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; -use sylvia::{contract, entry_points}; use crate::ContractError; @@ -83,7 +84,7 @@ pub struct InstantiateMsgData { pub minter: String, } -#[cfg_attr(not(feature = "library"), entry_points)] +#[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[error(ContractError)] #[messages(cw721_interface as Cw721Interface)] @@ -298,7 +299,7 @@ impl Cw721Contract<'_> { #[contract] #[messages(cw721_interface as Cw721Interface)] -impl cw721_interface::Cw721Interface for Cw721Contract<'_> { +impl Cw721Interface for Cw721Contract<'_> { type Error = ContractError; #[msg(exec)] diff --git a/contracts/cw721-sylvia-base/src/lib.rs b/contracts/cw721-sylvia-base/src/lib.rs index 226ea88b1..16def7734 100644 --- a/contracts/cw721-sylvia-base/src/lib.rs +++ b/contracts/cw721-sylvia-base/src/lib.rs @@ -1,4 +1,7 @@ pub mod contract; mod error; +#[cfg(test)] +mod multitest; + pub use crate::error::ContractError; diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs new file mode 100644 index 000000000..038c26235 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -0,0 +1,33 @@ +use crate::contract::{test_utils::Cw721Interface, InstantiateMsgData}; +use cw721::ContractInfoResponse; +use sylvia::multitest::App; + +use crate::contract::multitest_utils::CodeId; + +const CREATOR: &str = "creator"; + +#[test] +fn test_instantiate() { + let app = App::default(); + let code_id = CodeId::store_code(&app); + + let nft = code_id + .instantiate(InstantiateMsgData { + name: "Sylvia".to_string(), + symbol: "SYLVIA".to_string(), + minter: CREATOR.to_string(), + }) + .with_label("cw721-sylvia-base contract") + .call("addr0001") + .unwrap(); + + // Check contract metadata was set + let res = nft.cw721_interface_proxy().contract_info().unwrap(); + assert_eq!( + ContractInfoResponse { + name: "Sylvia".to_string(), + symbol: "SYLVIA".to_string() + }, + res + ); +} From 958400f9b5d405045807aa7cb7d682a8731b431c Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 19 Jun 2023 20:40:40 -0700 Subject: [PATCH 04/15] More tests --- contracts/cw721-sylvia-base/src/multitest.rs | 244 ++++++++++++++++++- 1 file changed, 238 insertions(+), 6 deletions(-) diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 038c26235..d88ee4ec4 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -1,33 +1,265 @@ -use crate::contract::{test_utils::Cw721Interface, InstantiateMsgData}; -use cw721::ContractInfoResponse; +use crate::{ + contract::{ + multitest_utils::Cw721ContractProxy, test_utils::Cw721Interface, InstantiateMsgData, + MinterResponse, + }, + ContractError, +}; +use cosmwasm_std::Empty; +use cw721::{ + ContractInfoResponse, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, +}; +use cw_ownable::OwnershipError; use sylvia::multitest::App; use crate::contract::multitest_utils::CodeId; const CREATOR: &str = "creator"; +const RANDOM: &str = "random"; + +pub struct TestCase<'a> { + nft_contract: Cw721ContractProxy<'a>, +} + +impl TestCase<'_> { + // Lifetimes with Sylvia are fun. Open to a better way of doing this + pub fn new<'b>(app: &'b App) -> TestCase<'b> { + let code_id = CodeId::store_code(app); + + TestCase::<'b> { + nft_contract: code_id + .instantiate(InstantiateMsgData { + name: "Sylvia".to_string(), + symbol: "SYLVIA".to_string(), + minter: CREATOR.to_string(), + }) + .with_label("cw721-sylvia-base contract") + .call(CREATOR) + .unwrap(), + } + } +} #[test] fn test_instantiate() { let app = App::default(); let code_id = CodeId::store_code(&app); - let nft = code_id + let nft_contract = code_id .instantiate(InstantiateMsgData { name: "Sylvia".to_string(), symbol: "SYLVIA".to_string(), minter: CREATOR.to_string(), }) .with_label("cw721-sylvia-base contract") - .call("addr0001") + .call(CREATOR) .unwrap(); // Check contract metadata was set - let res = nft.cw721_interface_proxy().contract_info().unwrap(); + let contract_info = nft_contract + .cw721_interface_proxy() + .contract_info() + .unwrap(); assert_eq!( ContractInfoResponse { name: "Sylvia".to_string(), symbol: "SYLVIA".to_string() }, - res + contract_info + ); + + // Query minter + let minter = nft_contract.minter().unwrap(); + assert_eq!( + MinterResponse { + minter: Some(CREATOR.to_string()) + }, + minter + ); + + // Check number of tokens is zero + let count = nft_contract.cw721_interface_proxy().num_tokens().unwrap(); + assert_eq!(NumTokensResponse { count: 0 }, count); +} + +#[test] +fn test_mint() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Only minter / owner can mint + let res = nft_contract + .mint( + "1".to_string(), + RANDOM.to_string(), + Some("https://example.com".to_string()), + ) + .call(RANDOM) + .unwrap_err(); + assert_eq!(res, ContractError::Ownership(OwnershipError::NotOwner)); + + // Minter / owner can mint an NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Check number of tokens is now one + let count = nft_contract.cw721_interface_proxy().num_tokens().unwrap(); + assert_eq!(NumTokensResponse { count: 1 }, count); + + // Query token info + let info = nft_contract + .cw721_interface_proxy() + .nft_info("1".to_string()) + .unwrap(); + assert_eq!( + NftInfoResponse:: { + token_uri: Some("https://example.com".to_string()), + extension: Empty {} + }, + info ); + + // Creator mints an NFT for a random address + nft_contract + .mint( + "2".to_string(), + RANDOM.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Query new tokens by owner + let tokens = nft_contract + .cw721_interface_proxy() + .tokens(CREATOR.to_string(), None, None) + .unwrap(); + assert_eq!( + TokensResponse { + tokens: vec!["1".to_string()] + }, + tokens + ); + + // Query all tokens + let all_tokens = nft_contract + .cw721_interface_proxy() + .all_tokens(None, None) + .unwrap(); + assert_eq!( + TokensResponse { + tokens: vec!["1".to_string(), "2".to_string()] + }, + all_tokens + ); +} + +#[test] +fn test_burn() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Mint NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Check number of tokens is now one + let count = nft_contract.cw721_interface_proxy().num_tokens().unwrap(); + assert_eq!(NumTokensResponse { count: 1 }, count); + + // Non minter / owner cannot burn + nft_contract + .cw721_interface_proxy() + .burn("1".to_string()) + .call(RANDOM) + .unwrap_err(); + + // Only owner can burn + nft_contract + .cw721_interface_proxy() + .burn("1".to_string()) + .call(CREATOR) + .unwrap(); + + // Token count has been reduced + let count = nft_contract.cw721_interface_proxy().num_tokens().unwrap(); + assert_eq!(NumTokensResponse { count: 0 }, count); +} + +#[test] +fn test_transfer() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Mint NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Owned by creator + let nft_ownership = nft_contract + .cw721_interface_proxy() + .owner_of("1".to_string(), false) + .unwrap(); + assert_eq!( + OwnerOfResponse { + owner: CREATOR.to_string(), + approvals: vec![] + }, + nft_ownership + ); + + // Random address can't transfer + let err = nft_contract + .cw721_interface_proxy() + .transfer_nft(RANDOM.to_string(), "1".to_string()) + .call(RANDOM) + .unwrap_err(); + assert_eq!(ContractError::Ownership(OwnershipError::NotOwner), err); + + // Only owner can transfer + nft_contract + .cw721_interface_proxy() + .transfer_nft(RANDOM.to_string(), "1".to_string()) + .call(CREATOR) + .unwrap(); + + // Ownership has changed + let nft_ownership = nft_contract + .cw721_interface_proxy() + .owner_of("1".to_string(), false) + .unwrap(); + assert_eq!( + OwnerOfResponse { + owner: RANDOM.to_string(), + approvals: vec![] + }, + nft_ownership + ); + + // Now that random is the owner, creator can't transfer it back + // even though they are the minter. + let err = nft_contract + .cw721_interface_proxy() + .transfer_nft(CREATOR.to_string(), "1".to_string()) + .call(CREATOR) + .unwrap_err(); + assert_eq!(ContractError::Ownership(OwnershipError::NotOwner), err); } From 54a435930dc33c6130f015f1ddb55929efa12d2c Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Mon, 19 Jun 2023 20:40:48 -0700 Subject: [PATCH 05/15] Clean up, add some comments --- contracts/cw721-sylvia-base/src/contract.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 3305528df..02316d779 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -14,9 +14,14 @@ use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; use crate::ContractError; +// Version info for migration +pub const CONTRACT_NAME: &str = "crates.io:cw721-sylvia-base"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + const DEFAULT_LIMIT: u32 = 10; const MAX_LIMIT: u32 = 100; +// TODO should this just be in cw721_interface along with the other responses? #[cw_serde] pub struct Approval { /// Account that can transfer/send the token @@ -31,12 +36,15 @@ impl Approval { } } +/// TODO move to a responses file? /// Shows who can mint these tokens #[cw_serde] pub struct MinterResponse { pub minter: Option, } +// TODO what do we want to do with extensions? They seem to be unnessary now? +// TODO kill extensions #[cw_serde] pub struct TokenInfo { /// The owner of the newly minted NFT @@ -107,7 +115,7 @@ impl Cw721Contract<'_> { ctx: InstantiateCtx, data: InstantiateMsgData, ) -> StdResult { - // TODO save contract metadata???? + cw2::set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let info = ContractInfoResponse { name: data.name, @@ -177,6 +185,8 @@ impl Cw721Contract<'_> { cw_ownable::get_ownership(ctx.deps.storage) } + // TODO figure out the best place for methods like this... + // Love sylvia, but could use help figuring out good patterns pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { let val = self.token_count.may_load(storage)?.unwrap_or_default() + 1; self.token_count.save(storage, &val)?; @@ -197,7 +207,7 @@ impl Cw721Contract<'_> { ) -> Result { let mut token = self.tokens.load(ctx.deps.storage, token_id)?; // ensure we have permissions - self.check_can_send(&ctx, &token)?; + self.check_can_send(ctx, &token)?; // set owner and remove existing approvals token.owner = ctx.deps.api.addr_validate(recipient)?; token.approvals = vec![]; @@ -217,7 +227,7 @@ impl Cw721Contract<'_> { ) -> Result { let mut token = self.tokens.load(ctx.deps.storage, token_id)?; // ensure we have permissions - self.check_can_approve(&ctx, &token)?; + self.check_can_approve(ctx, &token)?; // update the approval list (remove any for the same spender before adding) let spender_addr = ctx.deps.api.addr_validate(spender)?; @@ -297,6 +307,7 @@ impl Cw721Contract<'_> { } } +// TODO break up into separte file? base.rs? #[contract] #[messages(cw721_interface as Cw721Interface)] impl Cw721Interface for Cw721Contract<'_> { From 5102dd149da24426aae88395f5edaad0bc99803a Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 20 Jun 2023 02:32:58 -0700 Subject: [PATCH 06/15] Re-org files --- contracts/cw721-sylvia-base/src/base.rs | 518 ++++++++++++++++++ contracts/cw721-sylvia-base/src/contract.rs | 530 +------------------ contracts/cw721-sylvia-base/src/lib.rs | 1 + contracts/cw721-sylvia-base/src/multitest.rs | 6 +- 4 files changed, 531 insertions(+), 524 deletions(-) create mode 100644 contracts/cw721-sylvia-base/src/base.rs diff --git a/contracts/cw721-sylvia-base/src/base.rs b/contracts/cw721-sylvia-base/src/base.rs new file mode 100644 index 000000000..8f63b9953 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/base.rs @@ -0,0 +1,518 @@ +use cosmwasm_std::{Addr, Binary, BlockInfo, Empty, Order, Response, StdError, StdResult, Storage}; +use cw721::{ + cw721_interface, AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + Cw721ReceiveMsg, Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, + OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use cw721_interface::Cw721Interface; +use cw_ownable::OwnershipError; +use cw_storage_plus::Bound; +use cw_utils::maybe_addr; +use sylvia::contract; +use sylvia::types::{ExecCtx, QueryCtx}; + +use crate::contract::{Approval, Cw721Contract, TokenInfo, DEFAULT_LIMIT, MAX_LIMIT}; +use crate::ContractError; + +#[contract(module=crate::contract)] +#[messages(cw721_interface as Cw721Interface)] +impl Cw721Interface for Cw721Contract<'_> { + type Error = ContractError; + + #[msg(exec)] + fn transfer_nft( + &self, + mut ctx: ExecCtx, + recipient: String, + token_id: String, + ) -> Result { + self._transfer_nft(&mut ctx, &recipient, &token_id)?; + + Ok(Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", ctx.info.sender) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn send_nft( + &self, + mut ctx: ExecCtx, + contract: String, + token_id: String, + msg: Binary, + ) -> Result { + // Transfer token + self._transfer_nft(&mut ctx, &contract, &token_id)?; + + let send = Cw721ReceiveMsg { + sender: ctx.info.sender.to_string(), + token_id: token_id.clone(), + msg, + }; + + // Send message + Ok(Response::new() + .add_message(send.into_cosmos_msg(contract.clone())?) + .add_attribute("action", "send_nft") + .add_attribute("sender", ctx.info.sender) + .add_attribute("recipient", contract) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn approve( + &self, + mut ctx: ExecCtx, + spender: String, + token_id: String, + expires: Option, + ) -> Result { + self._update_approvals(&mut ctx, &spender, &token_id, true, expires)?; + + Ok(Response::new() + .add_attribute("action", "approve") + .add_attribute("sender", ctx.info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn revoke( + &self, + mut ctx: ExecCtx, + spender: String, + token_id: String, + ) -> Result { + self._update_approvals(&mut ctx, &spender, &token_id, false, None)?; + + Ok(Response::new() + .add_attribute("action", "revoke") + .add_attribute("sender", ctx.info.sender) + .add_attribute("spender", spender) + .add_attribute("token_id", token_id)) + } + + #[msg(exec)] + fn approve_all( + &self, + ctx: ExecCtx, + operator: String, + expires: Option, + ) -> Result { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&ctx.env.block) { + return Err(ContractError::Expired {}); + } + + // set the operator for us + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + self.operators.save( + ctx.deps.storage, + (&ctx.info.sender, &operator_addr), + &expires, + )?; + + Ok(Response::new() + .add_attribute("action", "approve_all") + .add_attribute("sender", ctx.info.sender) + .add_attribute("operator", operator)) + } + + #[msg(exec)] + fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result { + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + self.operators + .remove(ctx.deps.storage, (&ctx.info.sender, &operator_addr)); + + Ok(Response::new() + .add_attribute("action", "revoke_all") + .add_attribute("sender", ctx.info.sender) + .add_attribute("operator", operator)) + } + + #[msg(exec)] + fn burn(&self, ctx: ExecCtx, token_id: String) -> Result { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + self.check_can_send(&ctx, &token)?; + + self.tokens.remove(ctx.deps.storage, &token_id)?; + self.decrement_tokens(ctx.deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", ctx.info.sender) + .add_attribute("token_id", token_id)) + } + + #[msg(query)] + fn contract_info(&self, ctx: QueryCtx) -> StdResult { + self.contract_info.load(ctx.deps.storage) + } + + #[msg(query)] + fn num_tokens(&self, ctx: QueryCtx) -> StdResult { + let count = self + .token_count + .may_load(ctx.deps.storage)? + .unwrap_or_default(); + Ok(NumTokensResponse { count }) + } + + #[msg(query)] + fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult> { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(NftInfoResponse { + token_uri: info.token_uri, + extension: info.extension, + }) + } + + #[msg(query)] + fn owner_of( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&ctx.env.block, &info, include_expired), + }) + } + + /// operator returns the approval status of an operator for a given owner if exists + #[msg(query)] + fn operator( + &self, + ctx: QueryCtx, + owner: String, + operator: String, + include_expired: bool, + ) -> StdResult { + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let operator_addr = ctx.deps.api.addr_validate(&operator)?; + + let info = self + .operators + .may_load(ctx.deps.storage, (&owner_addr, &operator_addr))?; + + if let Some(expires) = info { + if !include_expired && expires.is_expired(&ctx.env.block) { + return Err(StdError::not_found("Approval not found")); + } + + return Ok(OperatorResponse { + approval: cw721::Approval { + spender: operator, + expires, + }, + }); + } + + Err(StdError::not_found("Approval not found")) + } + + /// operators returns all operators owner given access to + #[msg(query)] + fn operators( + &self, + ctx: QueryCtx, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_addr = maybe_addr(ctx.deps.api, start_after)?; + let start = start_addr.as_ref().map(Bound::exclusive); + + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let res: StdResult> = self + .operators + .prefix(&owner_addr) + .range(ctx.deps.storage, start, None, Order::Ascending) + .filter(|r| { + include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&ctx.env.block) + }) + .take(limit) + .map(parse_approval) + .collect(); + Ok(OperatorsResponse { operators: res? }) + } + + #[msg(query)] + fn approval( + &self, + ctx: QueryCtx, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + + // token owner has absolute approval + if token.owner == spender { + let approval = cw721::Approval { + spender: token.owner.to_string(), + expires: Expiration::Never {}, + }; + return Ok(ApprovalResponse { approval }); + } + + let filtered: Vec<_> = token + .approvals + .into_iter() + .filter(|t| t.spender == spender) + .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + if filtered.is_empty() { + return Err(StdError::not_found("Approval not found")); + } + // we expect only one item + let approval = filtered[0].clone(); + + Ok(ApprovalResponse { approval }) + } + + /// approvals returns all approvals owner given access to + #[msg(query)] + fn approvals( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult { + let token = self.tokens.load(ctx.deps.storage, &token_id)?; + let approvals: Vec<_> = token + .approvals + .into_iter() + .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) + .map(|a| cw721::Approval { + spender: a.spender.into_string(), + expires: a.expires, + }) + .collect(); + + Ok(ApprovalsResponse { approvals }) + } + + #[msg(query)] + fn tokens( + &self, + ctx: QueryCtx, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + let owner_addr = ctx.deps.api.addr_validate(&owner)?; + let tokens: Vec = self + .tokens + .idx + .owner + .prefix(owner_addr) + .keys(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + Ok(TokensResponse { tokens }) + } + + #[msg(query)] + fn all_tokens( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); + + let tokens: StdResult> = self + .tokens + .range(ctx.deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(k, _)| k)) + .collect(); + + Ok(TokensResponse { tokens: tokens? }) + } + + #[msg(query)] + fn all_nft_info( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult> { + let info = self.tokens.load(ctx.deps.storage, &token_id)?; + Ok(AllNftInfoResponse:: { + access: OwnerOfResponse { + owner: info.owner.to_string(), + approvals: humanize_approvals(&ctx.env.block, &info, include_expired), + }, + info: NftInfoResponse { + token_uri: info.token_uri, + extension: Empty {}, + }, + }) + } +} + +impl Cw721Contract<'_> { + pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count.may_load(storage)?.unwrap_or_default() + 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count.may_load(storage)?.unwrap_or_default() - 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn _transfer_nft( + &self, + ctx: &mut ExecCtx, + recipient: &str, + token_id: &str, + ) -> Result { + let mut token = self.tokens.load(ctx.deps.storage, token_id)?; + // ensure we have permissions + self.check_can_send(ctx, &token)?; + // set owner and remove existing approvals + token.owner = ctx.deps.api.addr_validate(recipient)?; + token.approvals = vec![]; + self.tokens.save(ctx.deps.storage, token_id, &token)?; + Ok(token) + } + + #[allow(clippy::too_many_arguments)] + pub fn _update_approvals( + &self, + ctx: &mut ExecCtx, + spender: &str, + token_id: &str, + // if add == false, remove. if add == true, remove then set with this expiration + add: bool, + expires: Option, + ) -> Result { + let mut token = self.tokens.load(ctx.deps.storage, token_id)?; + // ensure we have permissions + self.check_can_approve(ctx, &token)?; + + // update the approval list (remove any for the same spender before adding) + let spender_addr = ctx.deps.api.addr_validate(spender)?; + token.approvals.retain(|apr| apr.spender != spender_addr); + + // only difference between approve and revoke + if add { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&ctx.env.block) { + return Err(ContractError::Expired {}); + } + let approval = Approval { + spender: spender_addr, + expires, + }; + token.approvals.push(approval); + } + + self.tokens.save(ctx.deps.storage, token_id, &token)?; + + Ok(token) + } + + /// returns true iff the sender can execute approve or reject on the contract + pub fn check_can_approve(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { + // owner can approve + if token.owner == ctx.info.sender { + return Ok(()); + } + // operator can approve + let op = self + .operators + .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&ctx.env.block) { + Err(ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + } + } + + /// returns true iff the sender can transfer ownership of the token + pub fn check_can_send(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { + // owner can send + if token.owner == ctx.info.sender { + return Ok(()); + } + + // any non-expired token approval can send + if token + .approvals + .iter() + .any(|apr| apr.spender == ctx.info.sender && !apr.is_expired(&ctx.env.block)) + { + return Ok(()); + } + + // operator can send + let op = self + .operators + .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&ctx.env.block) { + Err(ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(ContractError::Ownership(OwnershipError::NotOwner)), + } + } +} + +pub fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + item.map(|(spender, expires)| cw721::Approval { + spender: spender.to_string(), + expires, + }) +} + +pub fn humanize_approvals( + block: &BlockInfo, + info: &TokenInfo, + include_expired: bool, +) -> Vec { + info.approvals + .iter() + .filter(|apr| include_expired || !apr.is_expired(block)) + .map(humanize_approval) + .collect() +} + +pub fn humanize_approval(approval: &Approval) -> cw721::Approval { + cw721::Approval { + spender: approval.spender.to_string(), + expires: approval.expires, + } +} diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 02316d779..22d698792 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -1,14 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Binary, BlockInfo, Empty, Order, Response, StdError, StdResult, Storage}; -use cw721::{ - cw721_interface, AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, - Cw721ReceiveMsg, Expiration, NftInfoResponse, NumTokensResponse, OperatorResponse, - OperatorsResponse, OwnerOfResponse, TokensResponse, -}; -use cw721_interface::Cw721Interface; -use cw_ownable::{Ownership, OwnershipError}; -use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, Item, Map, MultiIndex}; -use cw_utils::maybe_addr; +use cosmwasm_std::{Addr, BlockInfo, Empty, Response, StdResult}; +use cw721::{cw721_interface, ContractInfoResponse, Expiration}; +use cw_ownable::Ownership; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; @@ -18,8 +12,8 @@ use crate::ContractError; pub const CONTRACT_NAME: &str = "crates.io:cw721-sylvia-base"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 100; +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 100; // TODO should this just be in cw721_interface along with the other responses? #[cw_serde] @@ -60,6 +54,10 @@ pub struct TokenInfo { pub extension: Empty, } +pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { + d.owner.clone() +} + /// Indexed map for NFT tokens by owner pub struct TokenIndexes<'a> { pub owner: MultiIndex<'a, Addr, TokenInfo, String>, @@ -184,512 +182,4 @@ impl Cw721Contract<'_> { pub fn ownership(&self, ctx: QueryCtx) -> StdResult> { cw_ownable::get_ownership(ctx.deps.storage) } - - // TODO figure out the best place for methods like this... - // Love sylvia, but could use help figuring out good patterns - pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { - let val = self.token_count.may_load(storage)?.unwrap_or_default() + 1; - self.token_count.save(storage, &val)?; - Ok(val) - } - - pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { - let val = self.token_count.may_load(storage)?.unwrap_or_default() - 1; - self.token_count.save(storage, &val)?; - Ok(val) - } - - pub fn _transfer_nft( - &self, - ctx: &mut ExecCtx, - recipient: &str, - token_id: &str, - ) -> Result { - let mut token = self.tokens.load(ctx.deps.storage, token_id)?; - // ensure we have permissions - self.check_can_send(ctx, &token)?; - // set owner and remove existing approvals - token.owner = ctx.deps.api.addr_validate(recipient)?; - token.approvals = vec![]; - self.tokens.save(ctx.deps.storage, token_id, &token)?; - Ok(token) - } - - #[allow(clippy::too_many_arguments)] - pub fn _update_approvals( - &self, - ctx: &mut ExecCtx, - spender: &str, - token_id: &str, - // if add == false, remove. if add == true, remove then set with this expiration - add: bool, - expires: Option, - ) -> Result { - let mut token = self.tokens.load(ctx.deps.storage, token_id)?; - // ensure we have permissions - self.check_can_approve(ctx, &token)?; - - // update the approval list (remove any for the same spender before adding) - let spender_addr = ctx.deps.api.addr_validate(spender)?; - token.approvals.retain(|apr| apr.spender != spender_addr); - - // only difference between approve and revoke - if add { - // reject expired data as invalid - let expires = expires.unwrap_or_default(); - if expires.is_expired(&ctx.env.block) { - return Err(ContractError::Expired {}); - } - let approval = Approval { - spender: spender_addr, - expires, - }; - token.approvals.push(approval); - } - - self.tokens.save(ctx.deps.storage, token_id, &token)?; - - Ok(token) - } - - /// returns true iff the sender can execute approve or reject on the contract - pub fn check_can_approve(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { - // owner can approve - if token.owner == ctx.info.sender { - return Ok(()); - } - // operator can approve - let op = self - .operators - .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; - match op { - Some(ex) => { - if ex.is_expired(&ctx.env.block) { - Err(ContractError::Ownership(OwnershipError::NotOwner)) - } else { - Ok(()) - } - } - None => Err(ContractError::Ownership(OwnershipError::NotOwner)), - } - } - - /// returns true iff the sender can transfer ownership of the token - pub fn check_can_send(&self, ctx: &ExecCtx, token: &TokenInfo) -> Result<(), ContractError> { - // owner can send - if token.owner == ctx.info.sender { - return Ok(()); - } - - // any non-expired token approval can send - if token - .approvals - .iter() - .any(|apr| apr.spender == ctx.info.sender && !apr.is_expired(&ctx.env.block)) - { - return Ok(()); - } - - // operator can send - let op = self - .operators - .may_load(ctx.deps.storage, (&token.owner, &ctx.info.sender))?; - match op { - Some(ex) => { - if ex.is_expired(&ctx.env.block) { - Err(ContractError::Ownership(OwnershipError::NotOwner)) - } else { - Ok(()) - } - } - None => Err(ContractError::Ownership(OwnershipError::NotOwner)), - } - } -} - -// TODO break up into separte file? base.rs? -#[contract] -#[messages(cw721_interface as Cw721Interface)] -impl Cw721Interface for Cw721Contract<'_> { - type Error = ContractError; - - #[msg(exec)] - fn transfer_nft( - &self, - mut ctx: ExecCtx, - recipient: String, - token_id: String, - ) -> Result { - self._transfer_nft(&mut ctx, &recipient, &token_id)?; - - Ok(Response::new() - .add_attribute("action", "transfer_nft") - .add_attribute("sender", ctx.info.sender) - .add_attribute("recipient", recipient) - .add_attribute("token_id", token_id)) - } - - #[msg(exec)] - fn send_nft( - &self, - mut ctx: ExecCtx, - contract: String, - token_id: String, - msg: Binary, - ) -> Result { - // Transfer token - self._transfer_nft(&mut ctx, &contract, &token_id)?; - - let send = Cw721ReceiveMsg { - sender: ctx.info.sender.to_string(), - token_id: token_id.clone(), - msg, - }; - - // Send message - Ok(Response::new() - .add_message(send.into_cosmos_msg(contract.clone())?) - .add_attribute("action", "send_nft") - .add_attribute("sender", ctx.info.sender) - .add_attribute("recipient", contract) - .add_attribute("token_id", token_id)) - } - - #[msg(exec)] - fn approve( - &self, - mut ctx: ExecCtx, - spender: String, - token_id: String, - expires: Option, - ) -> Result { - self._update_approvals(&mut ctx, &spender, &token_id, true, expires)?; - - Ok(Response::new() - .add_attribute("action", "approve") - .add_attribute("sender", ctx.info.sender) - .add_attribute("spender", spender) - .add_attribute("token_id", token_id)) - } - - #[msg(exec)] - fn revoke( - &self, - mut ctx: ExecCtx, - spender: String, - token_id: String, - ) -> Result { - self._update_approvals(&mut ctx, &spender, &token_id, false, None)?; - - Ok(Response::new() - .add_attribute("action", "revoke") - .add_attribute("sender", ctx.info.sender) - .add_attribute("spender", spender) - .add_attribute("token_id", token_id)) - } - - #[msg(exec)] - fn approve_all( - &self, - ctx: ExecCtx, - operator: String, - expires: Option, - ) -> Result { - // reject expired data as invalid - let expires = expires.unwrap_or_default(); - if expires.is_expired(&ctx.env.block) { - return Err(ContractError::Expired {}); - } - - // set the operator for us - let operator_addr = ctx.deps.api.addr_validate(&operator)?; - self.operators.save( - ctx.deps.storage, - (&ctx.info.sender, &operator_addr), - &expires, - )?; - - Ok(Response::new() - .add_attribute("action", "approve_all") - .add_attribute("sender", ctx.info.sender) - .add_attribute("operator", operator)) - } - - #[msg(exec)] - fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result { - let operator_addr = ctx.deps.api.addr_validate(&operator)?; - self.operators - .remove(ctx.deps.storage, (&ctx.info.sender, &operator_addr)); - - Ok(Response::new() - .add_attribute("action", "revoke_all") - .add_attribute("sender", ctx.info.sender) - .add_attribute("operator", operator)) - } - - #[msg(exec)] - fn burn(&self, ctx: ExecCtx, token_id: String) -> Result { - let token = self.tokens.load(ctx.deps.storage, &token_id)?; - self.check_can_send(&ctx, &token)?; - - self.tokens.remove(ctx.deps.storage, &token_id)?; - self.decrement_tokens(ctx.deps.storage)?; - - Ok(Response::new() - .add_attribute("action", "burn") - .add_attribute("sender", ctx.info.sender) - .add_attribute("token_id", token_id)) - } - - #[msg(query)] - fn contract_info(&self, ctx: QueryCtx) -> StdResult { - self.contract_info.load(ctx.deps.storage) - } - - #[msg(query)] - fn num_tokens(&self, ctx: QueryCtx) -> StdResult { - let count = self - .token_count - .may_load(ctx.deps.storage)? - .unwrap_or_default(); - Ok(NumTokensResponse { count }) - } - - #[msg(query)] - fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult> { - let info = self.tokens.load(ctx.deps.storage, &token_id)?; - Ok(NftInfoResponse { - token_uri: info.token_uri, - extension: info.extension, - }) - } - - #[msg(query)] - fn owner_of( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult { - let info = self.tokens.load(ctx.deps.storage, &token_id)?; - Ok(OwnerOfResponse { - owner: info.owner.to_string(), - approvals: humanize_approvals(&ctx.env.block, &info, include_expired), - }) - } - - /// operator returns the approval status of an operator for a given owner if exists - #[msg(query)] - fn operator( - &self, - ctx: QueryCtx, - owner: String, - operator: String, - include_expired: bool, - ) -> StdResult { - let owner_addr = ctx.deps.api.addr_validate(&owner)?; - let operator_addr = ctx.deps.api.addr_validate(&operator)?; - - let info = self - .operators - .may_load(ctx.deps.storage, (&owner_addr, &operator_addr))?; - - if let Some(expires) = info { - if !include_expired && expires.is_expired(&ctx.env.block) { - return Err(StdError::not_found("Approval not found")); - } - - return Ok(OperatorResponse { - approval: cw721::Approval { - spender: operator, - expires, - }, - }); - } - - Err(StdError::not_found("Approval not found")) - } - - /// operators returns all operators owner given access to - #[msg(query)] - fn operators( - &self, - ctx: QueryCtx, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start_addr = maybe_addr(ctx.deps.api, start_after)?; - let start = start_addr.as_ref().map(Bound::exclusive); - - let owner_addr = ctx.deps.api.addr_validate(&owner)?; - let res: StdResult> = self - .operators - .prefix(&owner_addr) - .range(ctx.deps.storage, start, None, Order::Ascending) - .filter(|r| { - include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&ctx.env.block) - }) - .take(limit) - .map(parse_approval) - .collect(); - Ok(OperatorsResponse { operators: res? }) - } - - #[msg(query)] - fn approval( - &self, - ctx: QueryCtx, - token_id: String, - spender: String, - include_expired: bool, - ) -> StdResult { - let token = self.tokens.load(ctx.deps.storage, &token_id)?; - - // token owner has absolute approval - if token.owner == spender { - let approval = cw721::Approval { - spender: token.owner.to_string(), - expires: Expiration::Never {}, - }; - return Ok(ApprovalResponse { approval }); - } - - let filtered: Vec<_> = token - .approvals - .into_iter() - .filter(|t| t.spender == spender) - .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) - .map(|a| cw721::Approval { - spender: a.spender.into_string(), - expires: a.expires, - }) - .collect(); - - if filtered.is_empty() { - return Err(StdError::not_found("Approval not found")); - } - // we expect only one item - let approval = filtered[0].clone(); - - Ok(ApprovalResponse { approval }) - } - - /// approvals returns all approvals owner given access to - #[msg(query)] - fn approvals( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult { - let token = self.tokens.load(ctx.deps.storage, &token_id)?; - let approvals: Vec<_> = token - .approvals - .into_iter() - .filter(|t| include_expired || !t.is_expired(&ctx.env.block)) - .map(|a| cw721::Approval { - spender: a.spender.into_string(), - expires: a.expires, - }) - .collect(); - - Ok(ApprovalsResponse { approvals }) - } - - #[msg(query)] - fn tokens( - &self, - ctx: QueryCtx, - owner: String, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - let owner_addr = ctx.deps.api.addr_validate(&owner)?; - let tokens: Vec = self - .tokens - .idx - .owner - .prefix(owner_addr) - .keys(ctx.deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>>()?; - - Ok(TokensResponse { tokens }) - } - - #[msg(query)] - fn all_tokens( - &self, - ctx: QueryCtx, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into())); - - let tokens: StdResult> = self - .tokens - .range(ctx.deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| item.map(|(k, _)| k)) - .collect(); - - Ok(TokensResponse { tokens: tokens? }) - } - - #[msg(query)] - fn all_nft_info( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult> { - let info = self.tokens.load(ctx.deps.storage, &token_id)?; - Ok(AllNftInfoResponse:: { - access: OwnerOfResponse { - owner: info.owner.to_string(), - approvals: humanize_approvals(&ctx.env.block, &info, include_expired), - }, - info: NftInfoResponse { - token_uri: info.token_uri, - extension: Empty {}, - }, - }) - } -} - -pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { - d.owner.clone() -} - -pub fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { - item.map(|(spender, expires)| cw721::Approval { - spender: spender.to_string(), - expires, - }) -} - -pub fn humanize_approvals( - block: &BlockInfo, - info: &TokenInfo, - include_expired: bool, -) -> Vec { - info.approvals - .iter() - .filter(|apr| include_expired || !apr.is_expired(block)) - .map(humanize_approval) - .collect() -} - -pub fn humanize_approval(approval: &Approval) -> cw721::Approval { - cw721::Approval { - spender: approval.spender.to_string(), - expires: approval.expires, - } } diff --git a/contracts/cw721-sylvia-base/src/lib.rs b/contracts/cw721-sylvia-base/src/lib.rs index 16def7734..b84e73fd0 100644 --- a/contracts/cw721-sylvia-base/src/lib.rs +++ b/contracts/cw721-sylvia-base/src/lib.rs @@ -1,3 +1,4 @@ +mod base; pub mod contract; mod error; diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index d88ee4ec4..3c7ac6dd0 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -1,8 +1,5 @@ use crate::{ - contract::{ - multitest_utils::Cw721ContractProxy, test_utils::Cw721Interface, InstantiateMsgData, - MinterResponse, - }, + contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData, MinterResponse}, ContractError, }; use cosmwasm_std::Empty; @@ -12,6 +9,7 @@ use cw721::{ use cw_ownable::OwnershipError; use sylvia::multitest::App; +use crate::base::test_utils::Cw721Interface; use crate::contract::multitest_utils::CodeId; const CREATOR: &str = "creator"; From 6faf9751dc69b654a4eb590dcfd83f1fe2230042 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 21 Jun 2023 04:40:45 -0700 Subject: [PATCH 07/15] More reorganization and cleanup, add comments and basic README --- Cargo.lock | 2 +- Cargo.toml | 4 + contracts/cw721-base/README.md | 6 - contracts/cw721-sylvia-base/Cargo.toml | 15 +- contracts/cw721-sylvia-base/README.md | 8 ++ contracts/cw721-sylvia-base/src/base.rs | 3 +- contracts/cw721-sylvia-base/src/contract.rs | 88 ++++-------- contracts/cw721-sylvia-base/src/lib.rs | 6 +- contracts/cw721-sylvia-base/src/multitest.rs | 3 +- contracts/cw721-sylvia-base/src/responses.rs | 7 + contracts/cw721-sylvia-base/src/state.rs | 48 +++++++ packages/cw721/src/cw721_interface.rs | 141 ------------------ packages/cw721/src/interface.rs | 144 +++++++++++++++++++ packages/cw721/src/lib.rs | 3 +- 14 files changed, 257 insertions(+), 221 deletions(-) create mode 100644 contracts/cw721-sylvia-base/README.md create mode 100644 contracts/cw721-sylvia-base/src/responses.rs create mode 100644 contracts/cw721-sylvia-base/src/state.rs delete mode 100644 packages/cw721/src/cw721_interface.rs create mode 100644 packages/cw721/src/interface.rs diff --git a/Cargo.lock b/Cargo.lock index 8564cc052..d6dc57b54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,7 +504,7 @@ dependencies = [ [[package]] name = "cw721-sylvia-base" -version = "0.1.0" +version = "0.17.0" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/Cargo.toml b/Cargo.toml index 039bca76f..36a11e1c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,10 @@ incremental = false codegen-units = 1 incremental = false +[profile.release.package.cw721-sylvia-base] +codegen-units = 1 +incremental = false + [profile.release] rpath = false lto = true diff --git a/contracts/cw721-base/README.md b/contracts/cw721-base/README.md index d3be359b9..c53112114 100644 --- a/contracts/cw721-base/README.md +++ b/contracts/cw721-base/README.md @@ -62,9 +62,3 @@ This allows you to use custom `ExecuteMsg` and `QueryMsg` with your additional calls, but then use the underlying implementation for the standard cw721 messages you want to support. The same with `QueryMsg`. You will most likely want to write a custom, domain-specific `instantiate`. - -**TODO: add example when written** - -For now, you can look at [`cw721-staking`](../cw721-staking/README.md) -for an example of how to "inherit" cw721 functionality and combine it with custom logic. -The process is similar for cw721. diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml index b37a3b2fa..3baef412f 100644 --- a/contracts/cw721-sylvia-base/Cargo.toml +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -1,9 +1,14 @@ [package] -name = "cw721-sylvia-base" -version = "0.1.0" -edition = { workspace = true } -repository = { workspace = true } -license = { workspace = true } +name = "cw721-sylvia-base" +description = "Basic implementation of cw721 NFTs using the Sylvia framework." +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +rust-version = { workspace = true } + [features] library = [] diff --git a/contracts/cw721-sylvia-base/README.md b/contracts/cw721-sylvia-base/README.md new file mode 100644 index 000000000..528c8a07c --- /dev/null +++ b/contracts/cw721-sylvia-base/README.md @@ -0,0 +1,8 @@ +# cw721-sylvia-base + +This is a basic implementation of a cw721 NFT contract. It implements +the [CW721 spec](../../packages/cw721/README.md) and is designed to +be deployed as is, or imported into other contracts to easily build +cw721-compatible NFTs with custom logic. It uses the [Sylvia](https://github.com/cosmwasm/sylvia) smart contract framework for easier extension. + +Use this as a base to build your own custom NFT contract. diff --git a/contracts/cw721-sylvia-base/src/base.rs b/contracts/cw721-sylvia-base/src/base.rs index 8f63b9953..b55932b3e 100644 --- a/contracts/cw721-sylvia-base/src/base.rs +++ b/contracts/cw721-sylvia-base/src/base.rs @@ -11,7 +11,8 @@ use cw_utils::maybe_addr; use sylvia::contract; use sylvia::types::{ExecCtx, QueryCtx}; -use crate::contract::{Approval, Cw721Contract, TokenInfo, DEFAULT_LIMIT, MAX_LIMIT}; +use crate::contract::{Cw721Contract, DEFAULT_LIMIT, MAX_LIMIT}; +use crate::state::{Approval, TokenInfo}; use crate::ContractError; #[contract(module=crate::contract)] diff --git a/contracts/cw721-sylvia-base/src/contract.rs b/contracts/cw721-sylvia-base/src/contract.rs index 22d698792..8603225ed 100644 --- a/contracts/cw721-sylvia-base/src/contract.rs +++ b/contracts/cw721-sylvia-base/src/contract.rs @@ -1,82 +1,25 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, BlockInfo, Empty, Response, StdResult}; +use cosmwasm_std::{Addr, Empty, Response, StdResult}; use cw721::{cw721_interface, ContractInfoResponse, Expiration}; use cw_ownable::Ownership; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_storage_plus::{IndexedMap, Item, Map, MultiIndex}; use sylvia::contract; use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx}; +use crate::responses::MinterResponse; +use crate::state::{token_owner_idx, TokenIndexes, TokenInfo}; use crate::ContractError; // Version info for migration pub const CONTRACT_NAME: &str = "crates.io:cw721-sylvia-base"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Default limit for query pagination pub const DEFAULT_LIMIT: u32 = 10; +/// Maximum limit for query pagination pub const MAX_LIMIT: u32 = 100; -// TODO should this just be in cw721_interface along with the other responses? -#[cw_serde] -pub struct Approval { - /// Account that can transfer/send the token - pub spender: Addr, - /// When the Approval expires (maybe Expiration::never) - pub expires: Expiration, -} - -impl Approval { - pub fn is_expired(&self, block: &BlockInfo) -> bool { - self.expires.is_expired(block) - } -} - -/// TODO move to a responses file? -/// Shows who can mint these tokens -#[cw_serde] -pub struct MinterResponse { - pub minter: Option, -} - -// TODO what do we want to do with extensions? They seem to be unnessary now? -// TODO kill extensions -#[cw_serde] -pub struct TokenInfo { - /// The owner of the newly minted NFT - pub owner: Addr, - /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much - pub approvals: Vec, - - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - pub token_uri: Option, - - pub extension: Empty, -} - -pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { - d.owner.clone() -} - -/// Indexed map for NFT tokens by owner -pub struct TokenIndexes<'a> { - pub owner: MultiIndex<'a, Addr, TokenInfo, String>, -} -impl<'a> IndexList for TokenIndexes<'a> { - fn get_indexes(&'_ self) -> Box + '_)> + '_> { - let v: Vec<&dyn Index> = vec![&self.owner]; - Box::new(v.into_iter()) - } -} - -pub struct Cw721Contract<'a> { - pub contract_info: Item<'a, ContractInfoResponse>, - pub token_count: Item<'a, u64>, - /// Stored as (granter, operator) giving operator full control over granter's account - pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, - pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>>, -} - +/// The instantiation message data for this contract, used to set initial state #[cw_serde] pub struct InstantiateMsgData { /// Name of the NFT contract @@ -90,6 +33,17 @@ pub struct InstantiateMsgData { pub minter: String, } +/// The struct representing this contract, holds contract state. +/// See Sylvia docmentation for more info about customizing this. +pub struct Cw721Contract<'a> { + pub contract_info: Item<'a, ContractInfoResponse>, + pub token_count: Item<'a, u64>, + /// Stored as (granter, operator) giving operator full control over granter's account + pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, + pub tokens: IndexedMap<'a, &'a str, TokenInfo, TokenIndexes<'a>>, +} + +/// The actual contract implementation, base cw721 logic is implemented in base.rs #[cfg_attr(not(feature = "library"), sylvia::entry_points)] #[contract] #[error(ContractError)] @@ -183,3 +137,9 @@ impl Cw721Contract<'_> { cw_ownable::get_ownership(ctx.deps.storage) } } + +impl Default for Cw721Contract<'_> { + fn default() -> Self { + Self::new() + } +} diff --git a/contracts/cw721-sylvia-base/src/lib.rs b/contracts/cw721-sylvia-base/src/lib.rs index b84e73fd0..9d448947a 100644 --- a/contracts/cw721-sylvia-base/src/lib.rs +++ b/contracts/cw721-sylvia-base/src/lib.rs @@ -1,6 +1,10 @@ -mod base; +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod base; pub mod contract; mod error; +pub mod responses; +pub mod state; #[cfg(test)] mod multitest; diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 3c7ac6dd0..48e69c4e8 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -1,5 +1,5 @@ use crate::{ - contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData, MinterResponse}, + contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData}, ContractError, }; use cosmwasm_std::Empty; @@ -11,6 +11,7 @@ use sylvia::multitest::App; use crate::base::test_utils::Cw721Interface; use crate::contract::multitest_utils::CodeId; +use crate::responses::MinterResponse; const CREATOR: &str = "creator"; const RANDOM: &str = "random"; diff --git a/contracts/cw721-sylvia-base/src/responses.rs b/contracts/cw721-sylvia-base/src/responses.rs new file mode 100644 index 000000000..68eee6f59 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/responses.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; + +/// Shows who can mint these tokens +#[cw_serde] +pub struct MinterResponse { + pub minter: Option, +} diff --git a/contracts/cw721-sylvia-base/src/state.rs b/contracts/cw721-sylvia-base/src/state.rs new file mode 100644 index 000000000..fabe91a90 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/state.rs @@ -0,0 +1,48 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, BlockInfo, Empty}; +use cw_ownable::Expiration; +use cw_storage_plus::{Index, IndexList, MultiIndex}; + +#[cw_serde] +pub struct Approval { + /// Account that can transfer/send the token + pub spender: Addr, + /// When the Approval expires (maybe Expiration::never) + pub expires: Expiration, +} + +impl Approval { + pub fn is_expired(&self, block: &BlockInfo) -> bool { + self.expires.is_expired(block) + } +} + +#[cw_serde] +pub struct TokenInfo { + /// The owner of the newly minted NFT + pub owner: Addr, + /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much + pub approvals: Vec, + + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + + pub extension: Empty, +} + +pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { + d.owner.clone() +} + +/// Indexed map for NFT tokens by owner +pub struct TokenIndexes<'a> { + pub owner: MultiIndex<'a, Addr, TokenInfo, String>, +} +impl<'a> IndexList for TokenIndexes<'a> { + fn get_indexes(&'_ self) -> Box + '_)> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} diff --git a/packages/cw721/src/cw721_interface.rs b/packages/cw721/src/cw721_interface.rs deleted file mode 100644 index 7c8ab64c3..000000000 --- a/packages/cw721/src/cw721_interface.rs +++ /dev/null @@ -1,141 +0,0 @@ -use cosmwasm_std::{Binary, Empty, Response, StdResult}; -use cw_utils::Expiration; -use sylvia::cw_std::StdError; -use sylvia::interface; -use sylvia::types::{ExecCtx, QueryCtx}; - -use crate::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, - NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, -}; - -#[interface] -pub trait Cw721Interface { - type Error: From; - - #[msg(exec)] - fn transfer_nft( - &self, - ctx: ExecCtx, - recipient: String, - token_id: String, - ) -> Result; - - #[msg(exec)] - fn send_nft( - &self, - ctx: ExecCtx, - contract: String, - token_id: String, - msg: Binary, - ) -> Result; - - #[msg(exec)] - fn approve( - &self, - ctx: ExecCtx, - spender: String, - token_id: String, - expires: Option, - ) -> Result; - - #[msg(exec)] - fn revoke( - &self, - ctx: ExecCtx, - spender: String, - token_id: String, - ) -> Result; - - #[msg(exec)] - fn approve_all( - &self, - ctx: ExecCtx, - operator: String, - expires: Option, - ) -> Result; - - #[msg(exec)] - fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result; - - #[msg(exec)] - fn burn(&self, ctx: ExecCtx, token_id: String) -> Result; - - #[msg(query)] - fn contract_info(&self, ctx: QueryCtx) -> StdResult; - - #[msg(query)] - fn num_tokens(&self, ctx: QueryCtx) -> StdResult; - - #[msg(query)] - fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult>; - - #[msg(query)] - fn owner_of( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn operator( - &self, - ctx: QueryCtx, - owner: String, - operator: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn operators( - &self, - ctx: QueryCtx, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn approval( - &self, - ctx: QueryCtx, - token_id: String, - spender: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn approvals( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult; - - #[msg(query)] - fn tokens( - &self, - ctx: QueryCtx, - owner: String, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn all_tokens( - &self, - ctx: QueryCtx, - start_after: Option, - limit: Option, - ) -> StdResult; - - #[msg(query)] - fn all_nft_info( - &self, - ctx: QueryCtx, - token_id: String, - include_expired: bool, - ) -> StdResult>; -} diff --git a/packages/cw721/src/interface.rs b/packages/cw721/src/interface.rs new file mode 100644 index 000000000..6c30f08e3 --- /dev/null +++ b/packages/cw721/src/interface.rs @@ -0,0 +1,144 @@ +pub mod cw721_interface { + use cosmwasm_std::{Binary, Empty, Response, StdResult}; + use cw_utils::Expiration; + use sylvia::cw_std::StdError; + use sylvia::interface; + use sylvia::types::{ExecCtx, QueryCtx}; + + use crate::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, + NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, + }; + + #[interface] + pub trait Cw721Interface { + type Error: From; + + #[msg(exec)] + fn transfer_nft( + &self, + ctx: ExecCtx, + recipient: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn send_nft( + &self, + ctx: ExecCtx, + contract: String, + token_id: String, + msg: Binary, + ) -> Result; + + #[msg(exec)] + fn approve( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke( + &self, + ctx: ExecCtx, + spender: String, + token_id: String, + ) -> Result; + + #[msg(exec)] + fn approve_all( + &self, + ctx: ExecCtx, + operator: String, + expires: Option, + ) -> Result; + + #[msg(exec)] + fn revoke_all(&self, ctx: ExecCtx, operator: String) -> Result; + + #[msg(exec)] + fn burn(&self, ctx: ExecCtx, token_id: String) -> Result; + + #[msg(query)] + fn contract_info(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn num_tokens(&self, ctx: QueryCtx) -> StdResult; + + #[msg(query)] + fn nft_info(&self, ctx: QueryCtx, token_id: String) -> StdResult>; + + #[msg(query)] + fn owner_of( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operator( + &self, + ctx: QueryCtx, + owner: String, + operator: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn operators( + &self, + ctx: QueryCtx, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn approval( + &self, + ctx: QueryCtx, + token_id: String, + spender: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn approvals( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult; + + #[msg(query)] + fn tokens( + &self, + ctx: QueryCtx, + owner: String, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_tokens( + &self, + ctx: QueryCtx, + start_after: Option, + limit: Option, + ) -> StdResult; + + #[msg(query)] + fn all_nft_info( + &self, + ctx: QueryCtx, + token_id: String, + include_expired: bool, + ) -> StdResult>; + } +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 6ff22bb6e..e1ef09210 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,4 +1,4 @@ -pub mod cw721_interface; +mod interface; mod msg; mod query; mod receiver; @@ -6,6 +6,7 @@ mod traits; pub use cw_utils::Expiration; +pub use crate::interface::cw721_interface; pub use crate::msg::Cw721ExecuteMsg; pub use crate::query::{ AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, From 09e2bf37d8ab1cfa75fcf7863d042361bf86b684 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 28 Jun 2023 12:23:49 +0200 Subject: [PATCH 08/15] Add config file --- contracts/cw721-sylvia-base/config | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 contracts/cw721-sylvia-base/config diff --git a/contracts/cw721-sylvia-base/config b/contracts/cw721-sylvia-base/config new file mode 100644 index 000000000..7d1a066c8 --- /dev/null +++ b/contracts/cw721-sylvia-base/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" From 2da91eba63c29d332845194a983a3342eba53619 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 28 Jun 2023 16:50:50 +0200 Subject: [PATCH 09/15] Fix cw721-sylvia-base wasm build Requires setting `resolver = "2"` in the workspace. See: https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions --- Cargo.lock | 247 ++++++++++++++++++++++--- Cargo.toml | 2 +- contracts/cw721-sylvia-base/Cargo.toml | 1 - 3 files changed, 219 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6dc57b54..790e61527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,12 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base16ct" version = "0.2.0" @@ -113,9 +119,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" dependencies = [ "digest 0.10.7", - "ecdsa", + "ecdsa 0.16.9", "ed25519-zebra", - "k256", + "k256 0.13.2", "rand_core 0.6.4", "thiserror", ] @@ -184,6 +190,18 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -228,6 +246,25 @@ dependencies = [ "cosmwasm-std", ] +[[package]] +name = "cw-multi-test" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.10.5", + "k256 0.11.6", + "prost 0.9.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-multi-test" version = "0.19.0" @@ -429,7 +466,7 @@ version = "0.18.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.19.0", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -447,7 +484,7 @@ version = "0.18.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.19.0", "cw-ownable", "cw-storage-plus 1.2.0", "cw2 1.1.2", @@ -504,12 +541,11 @@ dependencies = [ [[package]] name = "cw721-sylvia-base" -version = "0.17.0" +version = "0.18.0" dependencies = [ - "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.19.0", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -521,6 +557,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.8" @@ -569,18 +615,30 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.8", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -604,21 +662,41 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.0", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -629,6 +707,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.0" @@ -667,13 +755,24 @@ dependencies = [ "wasi", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -751,6 +850,18 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + [[package]] name = "k256" version = "0.13.2" @@ -758,11 +869,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "once_cell", "sha2 0.10.8", - "signature", + "signature 2.2.0", ] [[package]] @@ -816,14 +927,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.8", + "spki 0.7.3", ] [[package]] @@ -869,6 +990,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive 0.9.0", +] + [[package]] name = "prost" version = "0.10.4" @@ -889,6 +1020,19 @@ dependencies = [ "prost-derive 0.12.3", ] +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "prost-derive" version = "0.10.1" @@ -939,6 +1083,17 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -979,16 +1134,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.8", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] @@ -1083,6 +1252,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -1093,6 +1272,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -1100,7 +1289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.8", ] [[package]] @@ -1124,7 +1313,7 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.5", "derivative", "konst", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 36a11e1c8..59cd19ec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = ["packages/*", "contracts/*"] +resolver = "2" [workspace.package] version = "0.18.0" @@ -11,7 +12,6 @@ documentation = "https://docs.cosmwasm.com" rust-version = "1.65" [workspace.dependencies] -anyhow = "1.0.68" cosmwasm-schema = "1.2.1" cosmwasm-std = "1.2.1" cw2 = "1.1.0" diff --git a/contracts/cw721-sylvia-base/Cargo.toml b/contracts/cw721-sylvia-base/Cargo.toml index 3baef412f..5ab2ad333 100644 --- a/contracts/cw721-sylvia-base/Cargo.toml +++ b/contracts/cw721-sylvia-base/Cargo.toml @@ -30,6 +30,5 @@ sylvia = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } cw-multi-test = { workspace = true } sylvia = { workspace = true, features = ["mt"] } From fe8a58e51dc052e8d495aa708346fef73e3c7092 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 28 Jun 2023 19:13:24 +0200 Subject: [PATCH 10/15] Add more test cases for approvals --- contracts/cw721-sylvia-base/src/multitest.rs | 375 ++++++++++++++++++- 1 file changed, 372 insertions(+), 3 deletions(-) diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 48e69c4e8..089d72e1c 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -2,11 +2,12 @@ use crate::{ contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData}, ContractError, }; -use cosmwasm_std::Empty; +use cosmwasm_std::{Addr, Empty, StdError}; use cw721::{ - ContractInfoResponse, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, + Approval, ApprovalResponse, ContractInfoResponse, NftInfoResponse, NumTokensResponse, + OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, }; -use cw_ownable::OwnershipError; +use cw_ownable::{Action, Expiration, OwnershipError}; use sylvia::multitest::App; use crate::base::test_utils::Cw721Interface; @@ -262,3 +263,371 @@ fn test_transfer() { .unwrap_err(); assert_eq!(ContractError::Ownership(OwnershipError::NotOwner), err); } + +#[test] +fn test_update_minter() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Minter Mints NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Update the owner to "random". The new owner should be able to + // mint new tokens, the old one should not. + nft_contract + .update_ownership(Action::TransferOwnership { + new_owner: RANDOM.to_string(), + expiry: None, + }) + .call(CREATOR) + .unwrap(); + + // Minter does not change until ownership transfer completes. + let minter = nft_contract.minter().unwrap(); + assert_eq!(minter.minter, Some(CREATOR.to_string())); + + // Pending ownership transfer should be discoverable via query. + let ownership = nft_contract.ownership().unwrap(); + + assert_eq!( + ownership, + cw_ownable::Ownership:: { + owner: Some(Addr::unchecked(CREATOR)), + pending_owner: Some(Addr::unchecked(RANDOM)), + pending_expiry: None, + } + ); + + // Accept the ownership transfer. + nft_contract + .update_ownership(Action::AcceptOwnership) + .call(RANDOM) + .unwrap(); + + // Minter changes after ownership transfer is accepted. + let minter = nft_contract.minter().unwrap(); + assert_eq!(minter.minter, Some(RANDOM.to_string())); + + // Old owner can not mint. + let err = nft_contract + .mint( + "2".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap_err(); + assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + + // New owner can mint. + nft_contract + .mint( + "2".to_string(), + RANDOM.to_string(), + Some("https://example.com".to_string()), + ) + .call(RANDOM) + .unwrap(); +} + +#[test] +fn test_approving_revoking() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Minter Mints NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Token owner shows in approval query + let res = nft_contract + .cw721_interface_proxy() + .approval("1".to_string(), CREATOR.to_string(), false) + .unwrap(); + + assert_eq!( + res, + ApprovalResponse { + approval: Approval { + spender: CREATOR.to_string(), + expires: Expiration::Never {} + } + } + ); + + // Give random transferring power + nft_contract + .cw721_interface_proxy() + .approve( + RANDOM.to_string(), + "1".to_string(), + Some(Expiration::AtHeight(1000000)), + ) + .call(CREATOR) + .unwrap(); + + // Test approval query + let res = nft_contract + .cw721_interface_proxy() + .approval("1".to_string(), RANDOM.to_string(), false) + .unwrap(); + assert_eq!( + res, + ApprovalResponse { + approval: Approval { + spender: String::from(RANDOM), + expires: Expiration::AtHeight(1000000) + } + } + ); + + // Random can now transfer NFT to its address + nft_contract + .cw721_interface_proxy() + .transfer_nft(RANDOM.to_string(), "1".to_string()) + .call(RANDOM) + .unwrap(); + + // Approvals are removed / cleared + let res = nft_contract + .cw721_interface_proxy() + .owner_of("1".to_string(), false) + .unwrap(); + assert_eq!( + res, + OwnerOfResponse { + owner: String::from(RANDOM), + approvals: vec![], + } + ); + + // Approve, revoke, and check for empty, to test revoke + nft_contract + .cw721_interface_proxy() + .approve(CREATOR.to_string(), "1".to_string(), None) + .call(RANDOM) + .unwrap(); + nft_contract + .cw721_interface_proxy() + .revoke(CREATOR.to_string(), "1".to_string()) + .call(RANDOM) + .unwrap(); + + // Approvals are now removed / cleared + let res = nft_contract + .cw721_interface_proxy() + .owner_of("1".to_string(), false) + .unwrap(); + + assert_eq!( + res, + OwnerOfResponse { + owner: String::from(RANDOM), + approvals: vec![], + } + ); +} + +#[test] +fn approving_all_revoking_all() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Minter Mints a couple NFTs for themselves + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + nft_contract + .mint( + "2".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Paginate token ids + let tokens = nft_contract + .cw721_interface_proxy() + .all_tokens(None, Some(1)) + .unwrap(); + assert_eq!(1, tokens.tokens.len()); + assert_eq!(vec!["1".to_string()], tokens.tokens); + let tokens = nft_contract + .cw721_interface_proxy() + .all_tokens(Some("1".to_string()), Some(3)) + .unwrap(); + assert_eq!(1, tokens.tokens.len()); + assert_eq!(vec!["2".to_string()], tokens.tokens); + + // Creator gives random full (operator) power over her tokens + nft_contract + .cw721_interface_proxy() + .approve_all(RANDOM.to_string(), None) + .call(CREATOR) + .unwrap(); + + // Random can now transfer + nft_contract + .cw721_interface_proxy() + .transfer_nft(RANDOM.to_string(), "1".to_string()) + .call(RANDOM) + .unwrap(); + + // TODO Random can now send (for now we just do a second transfer) + nft_contract + .cw721_interface_proxy() + .transfer_nft(RANDOM.to_string(), "2".to_string()) + .call(RANDOM) + .unwrap(); + + // Approve_all, revoke_all, and check for empty, to test revoke_all + nft_contract + .cw721_interface_proxy() + .approve_all(CREATOR.to_string(), Some(Expiration::AtHeight(1000000))) + .call(RANDOM) + .unwrap(); + + // Query for operator should return approvals + let res = nft_contract + .cw721_interface_proxy() + .operators(RANDOM.to_string(), false, None, None) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from(CREATOR), + expires: Expiration::AtHeight(1000000) + }] + } + ); + + // Second approval + nft_contract + .cw721_interface_proxy() + .approve_all("second".to_string(), Some(Expiration::AtHeight(1000000))) + .call(RANDOM) + .unwrap(); + + // Paginate queries + let res = nft_contract + .cw721_interface_proxy() + .operators(RANDOM.to_string(), true, None, Some(1)) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from(CREATOR), + expires: Expiration::AtHeight(1000000), + }] + } + ); + let res = nft_contract + .cw721_interface_proxy() + .operators(RANDOM.to_string(), true, Some(CREATOR.to_string()), Some(1)) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![cw721::Approval { + spender: String::from("second"), + expires: Expiration::AtHeight(1000000), + }] + } + ); + + // Test operator query + let res = nft_contract + .cw721_interface_proxy() + .operator(CREATOR.to_string(), RANDOM.to_string(), false) + .unwrap(); + assert_eq!( + res, + OperatorResponse { + approval: Approval { + spender: String::from(RANDOM), + expires: Expiration::Never {} + } + } + ); + + // Revoke all approvals for CREATOR + nft_contract + .cw721_interface_proxy() + .revoke_all(CREATOR.to_string()) + .call(RANDOM) + .unwrap(); + + // Approvals are removed / cleared without affecting others + let res = nft_contract + .cw721_interface_proxy() + .operators(RANDOM.to_string(), false, None, None) + .unwrap(); + assert_eq!( + res, + OperatorsResponse { + operators: vec![Approval { + spender: "second".to_string(), + expires: Expiration::AtHeight(1000000) + }] + } + ); + + // Query for old operator should return error + let res = nft_contract.cw721_interface_proxy().operator( + RANDOM.to_string(), + CREATOR.to_string(), + false, + ); + match res { + Err(ContractError::Std(StdError::GenericErr { msg })) => { + assert_eq!(msg, "Querier contract error: Approval not found not found") + } + _ => panic!("Unexpected error"), + } + + // Ensure the filter works (approvals should expire) + let mut block = nft_contract.app.block_info(); + block.height += 1000000; + nft_contract.app.set_block(block); + + let res = nft_contract + .cw721_interface_proxy() + .operators(RANDOM.to_string(), false, None, None) + .unwrap(); + assert_eq!(0, res.operators.len()); + + // Query operator should also return error + let res = nft_contract.cw721_interface_proxy().operator( + RANDOM.to_string(), + CREATOR.to_string(), + false, + ); + match res { + Err(ContractError::Std(StdError::GenericErr { msg })) => { + assert_eq!(msg, "Querier contract error: Approval not found not found") + } + _ => panic!("Unexpected error"), + } +} From c7b5ceaf1c9d422b5d13eabdea2231f3ffa58d17 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 28 Jun 2023 20:05:57 +0200 Subject: [PATCH 11/15] Tests for sends --- contracts/cw721-sylvia-base/src/multitest.rs | 102 ++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 089d72e1c..2d2014cfd 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -2,7 +2,7 @@ use crate::{ contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData}, ContractError, }; -use cosmwasm_std::{Addr, Empty, StdError}; +use cosmwasm_std::{to_binary, Addr, Empty, StdError}; use cw721::{ Approval, ApprovalResponse, ContractInfoResponse, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, @@ -264,6 +264,62 @@ fn test_transfer() { assert_eq!(ContractError::Ownership(OwnershipError::NotOwner), err); } +#[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: error executing WasmMsg:\nsender: creator\nExecute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" +)] +#[test] +fn test_send() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Mint NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Owned by creator + let nft_ownership = nft_contract + .cw721_interface_proxy() + .owner_of("1".to_string(), false) + .unwrap(); + assert_eq!( + OwnerOfResponse { + owner: CREATOR.to_string(), + approvals: vec![] + }, + nft_ownership + ); + + // Random address can't send + let err = nft_contract + .cw721_interface_proxy() + .send_nft( + nft_contract.contract_addr.clone().into_string(), + "1".to_string(), + to_binary(&Empty {}).unwrap(), + ) + .call(RANDOM) + .unwrap_err(); + assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + + // Creator can send + // This will panic as the base contract does not implement RecieveNft + nft_contract + .cw721_interface_proxy() + .send_nft( + nft_contract.contract_addr.into_string(), + "1".to_string(), + to_binary(&Empty {}).unwrap(), + ) + .call(CREATOR) + .unwrap_err(); +} + #[test] fn test_update_minter() { let app = App::default(); @@ -493,7 +549,7 @@ fn approving_all_revoking_all() { .call(RANDOM) .unwrap(); - // TODO Random can now send (for now we just do a second transfer) + // Random can now transfer nft_contract .cw721_interface_proxy() .transfer_nft(RANDOM.to_string(), "2".to_string()) @@ -631,3 +687,45 @@ fn approving_all_revoking_all() { _ => panic!("Unexpected error"), } } + +#[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: error executing WasmMsg:\nsender: creator\nExecute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" +)] +#[test] +fn test_send_with_approval() { + let app = App::default(); + let TestCase { nft_contract } = TestCase::new(&app); + + // Mint NFT + nft_contract + .mint( + "1".to_string(), + CREATOR.to_string(), + Some("https://example.com".to_string()), + ) + .call(CREATOR) + .unwrap(); + + // Grant approval + nft_contract + .cw721_interface_proxy() + .approve( + RANDOM.to_string(), + "1".to_string(), + Some(Expiration::Never {}), + ) + .call(CREATOR) + .unwrap(); + + // Random address can send + // This will panic as the base contract does not implement RecieveNft + nft_contract + .cw721_interface_proxy() + .send_nft( + nft_contract.contract_addr.into_string(), + "1".to_string(), + to_binary(&Empty {}).unwrap(), + ) + .call(CREATOR) + .unwrap_err(); +} From aa7db50c93f5078e6315ef043da98bfda00e9493 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Tue, 7 Nov 2023 16:45:29 +0100 Subject: [PATCH 12/15] Update Sylvia to latest version --- Cargo.lock | 265 +++---------------- Cargo.toml | 2 +- contracts/cw721-sylvia-base/src/multitest.rs | 30 ++- 3 files changed, 59 insertions(+), 238 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 790e61527..aa286fbff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,12 +19,6 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - [[package]] name = "base16ct" version = "0.2.0" @@ -119,9 +113,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" dependencies = [ "digest 0.10.7", - "ecdsa 0.16.9", + "ecdsa", "ed25519-zebra", - "k256 0.13.2", + "k256", "rand_core 0.6.4", "thiserror", ] @@ -175,7 +169,7 @@ dependencies = [ "hex", "schemars", "serde", - "serde-json-wasm", + "serde-json-wasm 0.5.1", "sha2 0.10.8", "static_assertions", "thiserror", @@ -190,18 +184,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -246,25 +228,6 @@ dependencies = [ "cosmwasm-std", ] -[[package]] -name = "cw-multi-test" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" -dependencies = [ - "anyhow", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "cw-utils 1.0.3", - "derivative", - "itertools 0.10.5", - "k256 0.11.6", - "prost 0.9.0", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw-multi-test" version = "0.19.0" @@ -466,7 +429,7 @@ version = "0.18.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.19.0", + "cw-multi-test", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -484,7 +447,7 @@ version = "0.18.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.19.0", + "cw-multi-test", "cw-ownable", "cw-storage-plus 1.2.0", "cw2 1.1.2", @@ -545,7 +508,7 @@ version = "0.18.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.19.0", + "cw-multi-test", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -557,16 +520,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - [[package]] name = "der" version = "0.7.8" @@ -615,30 +568,18 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] - [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.8", + "der", "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] @@ -662,41 +603,21 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest 0.10.7", - "ff 0.12.1", - "generic-array", - "group 0.12.1", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1 0.3.0", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", + "base16ct", + "crypto-bigint", "digest 0.10.7", - "ff 0.13.0", + "ff", "generic-array", - "group 0.13.0", - "pkcs8 0.10.2", + "group", + "pkcs8", "rand_core 0.6.4", - "sec1 0.7.3", + "sec1", "subtle", "zeroize", ] @@ -707,16 +628,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "ff" version = "0.13.0" @@ -755,24 +666,13 @@ dependencies = [ "wasi", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.0", + "ff", "rand_core 0.6.4", "subtle", ] @@ -850,18 +750,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "k256" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" -dependencies = [ - "cfg-if", - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2 0.10.8", -] - [[package]] name = "k256" version = "0.13.2" @@ -869,11 +757,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "once_cell", "sha2 0.10.8", - "signature 2.2.0", + "signature", ] [[package]] @@ -927,24 +815,14 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.8", - "spki 0.7.3", + "der", + "spki", ] [[package]] @@ -990,16 +868,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes", - "prost-derive 0.9.0", -] - [[package]] name = "prost" version = "0.10.4" @@ -1020,19 +888,6 @@ dependencies = [ "prost-derive 0.12.3", ] -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "prost-derive" version = "0.10.1" @@ -1083,17 +938,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -1134,30 +978,16 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.8", + "base16ct", + "der", "generic-array", - "pkcs8 0.10.2", + "pkcs8", "subtle", "zeroize", ] @@ -1195,6 +1025,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-json-wasm" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c37d03f3b0f6b5f77c11af1e7c772de1c9af83e50bef7bb6069601900ba67b" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.193" @@ -1252,16 +1091,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "2.2.0" @@ -1272,16 +1101,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - [[package]] name = "spki" version = "0.7.3" @@ -1289,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.8", + "der", ] [[package]] @@ -1306,28 +1125,28 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "sylvia" -version = "0.5.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fc0aae39bc8a76742c1455e3bda09976224e31d236de0e468c91fbc2414c79" +checksum = "faef78eb230030dfae1c4e82c8c150d4166c2799961330bb2df261ba60a854ba" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.16.5", + "cw-multi-test", "derivative", "konst", "schemars", "serde", "serde-cw-value", - "serde-json-wasm", + "serde-json-wasm 1.0.0", "sylvia-derive", ] [[package]] name = "sylvia-derive" -version = "0.5.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c50a057bb2787c04dd93a203809e79d424d5de21b5024e23bc8d1cd5e6f34d4" +checksum = "d4c0a54180e1b49f8bb3134ad15ecc7100d4669235b23b9be1d9a609d6f037a2" dependencies = [ "convert_case", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index 59cd19ec1..bdad149d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ cw-storage-plus = "1.1.0" cw-utils = "1.0.1" schemars = "0.8.11" serde = { version = "1.0.152", default-features = false, features = ["derive"] } -sylvia = "0.5.0" +sylvia = "0.9.2" thiserror = "1.0.38" [profile.release.package.cw721-base] diff --git a/contracts/cw721-sylvia-base/src/multitest.rs b/contracts/cw721-sylvia-base/src/multitest.rs index 2d2014cfd..e64463e34 100644 --- a/contracts/cw721-sylvia-base/src/multitest.rs +++ b/contracts/cw721-sylvia-base/src/multitest.rs @@ -1,29 +1,30 @@ -use crate::{ - contract::{multitest_utils::Cw721ContractProxy, InstantiateMsgData}, - ContractError, -}; -use cosmwasm_std::{to_binary, Addr, Empty, StdError}; +use cosmwasm_std::{to_json_binary, Addr, Empty, StdError}; use cw721::{ Approval, ApprovalResponse, ContractInfoResponse, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, }; +use cw_multi_test::App as MtApp; use cw_ownable::{Action, Expiration, OwnershipError}; use sylvia::multitest::App; -use crate::base::test_utils::Cw721Interface; -use crate::contract::multitest_utils::CodeId; +use crate::base::sv::test_utils::Cw721Interface; use crate::responses::MinterResponse; +use crate::{ + contract::sv::multitest_utils::{CodeId, Cw721ContractProxy}, + contract::InstantiateMsgData, + ContractError, +}; const CREATOR: &str = "creator"; const RANDOM: &str = "random"; pub struct TestCase<'a> { - nft_contract: Cw721ContractProxy<'a>, + nft_contract: Cw721ContractProxy<'a, MtApp>, } impl TestCase<'_> { // Lifetimes with Sylvia are fun. Open to a better way of doing this - pub fn new<'b>(app: &'b App) -> TestCase<'b> { + pub fn new<'b>(app: &'b App) -> TestCase<'b> { let code_id = CodeId::store_code(app); TestCase::<'b> { @@ -264,8 +265,9 @@ fn test_transfer() { assert_eq!(ContractError::Ownership(OwnershipError::NotOwner), err); } +// NOTE: when a reciever test contract is implemented, we don't have to panic here. #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: error executing WasmMsg:\nsender: creator\nExecute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" + expected = "called `Result::unwrap()` on an `Err` value: Error executing WasmMsg:\n sender: creator\n Execute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: Error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::sv::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" )] #[test] fn test_send() { @@ -301,7 +303,7 @@ fn test_send() { .send_nft( nft_contract.contract_addr.clone().into_string(), "1".to_string(), - to_binary(&Empty {}).unwrap(), + to_json_binary(&Empty {}).unwrap(), ) .call(RANDOM) .unwrap_err(); @@ -314,7 +316,7 @@ fn test_send() { .send_nft( nft_contract.contract_addr.into_string(), "1".to_string(), - to_binary(&Empty {}).unwrap(), + to_json_binary(&Empty {}).unwrap(), ) .call(CREATOR) .unwrap_err(); @@ -689,7 +691,7 @@ fn approving_all_revoking_all() { } #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: error executing WasmMsg:\nsender: creator\nExecute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" + expected = "called `Result::unwrap()` on an `Err` value: Error executing WasmMsg:\n sender: creator\n Execute { contract_addr: \"contract0\", msg: {\"send_nft\":{\"contract\":\"contract0\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n\nCaused by:\n 0: Error executing WasmMsg:\n sender: contract0\n Execute { contract_addr: \"contract0\", msg: {\"receive_nft\":{\"sender\":\"creator\",\"token_id\":\"1\",\"msg\":\"e30=\"}}, funds: [] }\n 1: Error parsing into type cw721_sylvia_base::contract::sv::ContractExecMsg: Unsupported message received: {\"receive_nft\":{\"msg\":\"e30=\",\"sender\":\"creator\",\"token_id\":\"1\"}}. Messages supported by this contract: approve, approve_all, burn, revoke, revoke_all, send_nft, transfer_nft, mint, update_ownership" )] #[test] fn test_send_with_approval() { @@ -724,7 +726,7 @@ fn test_send_with_approval() { .send_nft( nft_contract.contract_addr.into_string(), "1".to_string(), - to_binary(&Empty {}).unwrap(), + to_json_binary(&Empty {}).unwrap(), ) .call(CREATOR) .unwrap_err(); From 9c6b8841d2c6c68cfda3f561fbf728b12d4cf070 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 7 Dec 2023 17:27:44 -0800 Subject: [PATCH 13/15] Add schema --- .../cw721-sylvia-base/{ => .cargo}/config | 2 +- .../schema/cw721-sylvia-base.json | 1616 +++++++++++++++++ contracts/cw721-sylvia-base/src/bin/schema.rs | 12 + 3 files changed, 1629 insertions(+), 1 deletion(-) rename contracts/cw721-sylvia-base/{ => .cargo}/config (81%) create mode 100644 contracts/cw721-sylvia-base/schema/cw721-sylvia-base.json create mode 100644 contracts/cw721-sylvia-base/src/bin/schema.rs diff --git a/contracts/cw721-sylvia-base/config b/contracts/cw721-sylvia-base/.cargo/config similarity index 81% rename from contracts/cw721-sylvia-base/config rename to contracts/cw721-sylvia-base/.cargo/config index 7d1a066c8..08d13a665 100644 --- a/contracts/cw721-sylvia-base/config +++ b/contracts/cw721-sylvia-base/.cargo/config @@ -2,4 +2,4 @@ wasm = "build --release --target wasm32-unknown-unknown" wasm-debug = "build --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example schema" +schema = "run --bin schema" diff --git a/contracts/cw721-sylvia-base/schema/cw721-sylvia-base.json b/contracts/cw721-sylvia-base/schema/cw721-sylvia-base.json new file mode 100644 index 000000000..63a871602 --- /dev/null +++ b/contracts/cw721-sylvia-base/schema/cw721-sylvia-base.json @@ -0,0 +1,1616 @@ +{ + "contract_name": "cw721-sylvia-base", + "contract_version": "0.18.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "$ref": "#/definitions/InstantiateMsgData" + } + }, + "definitions": { + "InstantiateMsgData": { + "description": "The instantiation message data for this contract, used to set initial state", + "type": "object", + "required": [ + "minter", + "name", + "symbol" + ], + "properties": { + "minter": { + "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", + "type": "string" + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "anyOf": [ + { + "$ref": "#/definitions/Cw721InterfaceExecMsg" + }, + { + "$ref": "#/definitions/ExecMsg" + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw721InterfaceExecMsg": { + "oneOf": [ + { + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "ExecMsg": { + "oneOf": [ + { + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "owner": { + "type": "string" + }, + "token_id": { + "type": "string" + }, + "token_uri": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "$ref": "#/definitions/Action" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "anyOf": [ + { + "$ref": "#/definitions/Cw721InterfaceQueryMsg" + }, + { + "$ref": "#/definitions/QueryMsg" + } + ], + "definitions": { + "Cw721InterfaceQueryMsg": { + "oneOf": [ + { + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "include_expired", + "token_id" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "include_expired", + "operator", + "owner" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "object", + "required": [ + "include_expired", + "owner" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "include_expired", + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "include_expired", + "token_id" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "include_expired", + "token_id" + ], + "properties": { + "include_expired": { + "type": "boolean" + }, + "token_id": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] + }, + "QueryMsg": { + "oneOf": [ + { + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "all_nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse_for_Empty", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_Empty" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NftInfoResponse_for_Empty": { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/Empty" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "all_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "approval": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "approvals": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "contract_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "additionalProperties": false + }, + "minter": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "description": "Shows who can mint these tokens", + "type": "object", + "properties": { + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse_for_Empty", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/Empty" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + } + } + }, + "num_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "operator": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "operators": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "owner_of": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_Addr", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/cw721-sylvia-base/src/bin/schema.rs b/contracts/cw721-sylvia-base/src/bin/schema.rs new file mode 100644 index 000000000..ba6047a13 --- /dev/null +++ b/contracts/cw721-sylvia-base/src/bin/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use cw721_sylvia_base::contract::sv::{ContractExecMsg, ContractQueryMsg, InstantiateMsg}; + +#[cfg(not(tarpaulin_include))] +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ContractExecMsg, + query: ContractQueryMsg, + } +} From 05e2e66e6c64a08a52e247b0bbf7495e6397ccb2 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 7 Dec 2023 17:28:42 -0800 Subject: [PATCH 14/15] Bump rust version in Circle CI to 1.69.0 Needed for latest sylvia version it seems --- .circleci/config.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f72b98179..3a2e584a3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ workflows: jobs: contract_cw721_base: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 working_directory: ~/project/contracts/cw721-base steps: - checkout: @@ -31,7 +31,7 @@ jobs: command: rustc --version; cargo --version; rustup --version - restore_cache: keys: - - cargocache-cw721-base-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + - cargocache-cw721-base-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -53,11 +53,11 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-base-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-base-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} contract_cw721_metadata_onchain: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 working_directory: ~/project/contracts/cw721-metadata-onchain steps: - checkout: @@ -67,7 +67,7 @@ jobs: command: rustc --version; cargo --version; rustup --version - restore_cache: keys: - - cargocache-cw721-metadata-onchain-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + - cargocache-cw721-metadata-onchain-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -89,11 +89,11 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-metadata-onchain-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-metadata-onchain-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} contract_cw721_fixed_price: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 working_directory: ~/project/contracts/cw721-fixed-price steps: - checkout: @@ -103,7 +103,7 @@ jobs: command: rustc --version; cargo --version; rustup --version - restore_cache: keys: - - cargocache-cw721-fixed-price-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + - cargocache-cw721-fixed-price-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -125,11 +125,11 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-fixed-price-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-fixed-price-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} package_cw721: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 working_directory: ~/project/packages/cw721 steps: - checkout: @@ -166,7 +166,7 @@ jobs: lint: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 steps: - checkout - run: @@ -174,7 +174,7 @@ jobs: command: rustc --version; cargo --version; rustup --version; rustup target list --installed - restore_cache: keys: - - cargocache-v2-lint-rust:1.65.0-{{ checksum "Cargo.lock" }} + - cargocache-v2-lint-rust:1.69.0-{{ checksum "Cargo.lock" }} - run: name: Add rustfmt component command: rustup component add rustfmt @@ -193,7 +193,7 @@ jobs: - target/debug/.fingerprint - target/debug/build - target/debug/deps - key: cargocache-v2-lint-rust:1.65.0-{{ checksum "Cargo.lock" }} + key: cargocache-v2-lint-rust:1.69.0-{{ checksum "Cargo.lock" }} # This runs one time on the top level to ensure all contracts compile properly into wasm. # We don't run the wasm build per contract build, and then reuse a lot of the same dependencies, so this speeds up CI time @@ -201,7 +201,7 @@ jobs: # We also sanity-check the resultant wasm files. wasm-build: docker: - - image: rust:1.65.0 + - image: rust:1.69.0 steps: - checkout: path: ~/project @@ -210,7 +210,7 @@ jobs: command: rustc --version; cargo --version; rustup --version - restore_cache: keys: - - cargocache-wasm-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + - cargocache-wasm-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Add wasm32 target command: rustup target add wasm32-unknown-unknown @@ -230,7 +230,7 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-wasm-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-wasm-rust:1.69.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Check wasm contracts command: cosmwasm-check ./target/wasm32-unknown-unknown/release/*.wasm From 22f002bec4c8c57ec210b50f7cc3b5eb7d00010e Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Thu, 7 Dec 2023 19:09:36 -0800 Subject: [PATCH 15/15] Fix wasm build --- contracts/cw721-sylvia-base/.cargo/config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/cw721-sylvia-base/.cargo/config b/contracts/cw721-sylvia-base/.cargo/config index 08d13a665..6e6a3fe57 100644 --- a/contracts/cw721-sylvia-base/.cargo/config +++ b/contracts/cw721-sylvia-base/.cargo/config @@ -1,5 +1,5 @@ [alias] -wasm = "build --release --target wasm32-unknown-unknown" -wasm-debug = "build --target wasm32-unknown-unknown" +wasm = "build --release --target wasm32-unknown-unknown --lib" +wasm-debug = "build --target wasm32-unknown-unknown --lib" unit-test = "test --lib" schema = "run --bin schema"