diff --git a/.circleci/config.yml b/.circleci/config.yml index 0a4c01546..513fc597b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,8 +7,9 @@ workflows: - contract_cw1155_metadata_onchain - contract_cw1155_royalties - contract_cw721_base - - contract_cw721_metadata_onchain + - contract_cw721_expiration - contract_cw721_fixed_price + - contract_cw721_receiver_tester - package_cw721 - package_cw1155 - lint @@ -133,7 +134,7 @@ jobs: contract_cw721_base: docker: - - image: rust:1.65.0 + - image: rust:1.78.0 working_directory: ~/project/contracts/cw721-base steps: - checkout: @@ -143,7 +144,31 @@ 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.78.0-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Unit Tests + environment: + RUST_BACKTRACE: 1 + command: cargo unit-test --locked + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-cw721-base-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} + + contract_cw721_expiration: + docker: + - image: rust:1.78.0 + working_directory: ~/project/contracts/cw721-expiration + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - cargocache-cw721-expiration-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -165,12 +190,12 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-base-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-expiration-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} - contract_cw721_metadata_onchain: + contract_cw721_fixed_price: docker: - - image: rust:1.65.0 - working_directory: ~/project/contracts/cw721-metadata-onchain + - image: rust:1.78.0 + working_directory: ~/project/contracts/cw721-fixed-price steps: - checkout: path: ~/project @@ -179,7 +204,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-fixed-price-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -201,12 +226,12 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-metadata-onchain-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-fixed-price-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} - contract_cw721_fixed_price: + contract_cw721_receiver_tester: docker: - - image: rust:1.65.0 - working_directory: ~/project/contracts/cw721-fixed-price + - image: rust:1.78.0 + working_directory: ~/project/contracts/cw721-receiver-tester steps: - checkout: path: ~/project @@ -215,7 +240,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-receiver-tester-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} - run: name: Unit Tests environment: @@ -237,11 +262,11 @@ jobs: paths: - /usr/local/cargo/registry - target - key: cargocache-cw721-fixed-price-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + key: cargocache-cw721-receiver-tester-rust:1.78.0-{{ checksum "~/project/Cargo.lock" }} package_cw721: docker: - - image: rust:1.65.0 + - image: rust:1.78.0 working_directory: ~/project/packages/cw721 steps: - checkout: @@ -315,7 +340,7 @@ jobs: lint: docker: - - image: rust:1.65.0 + - image: rust:1.78.0 steps: - checkout - run: @@ -323,7 +348,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.78.0-{{ checksum "Cargo.lock" }} - run: name: Add rustfmt component command: rustup component add rustfmt @@ -342,7 +367,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.78.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 @@ -350,39 +375,53 @@ jobs: # We also sanity-check the resultant wasm files. wasm-build: docker: - - image: rust:1.65.0 + # Image from https://github.com/cibuilds/github, based on alpine + - image: cibuilds/github:0.13 steps: - - checkout: - path: ~/project - run: - name: Version information - command: rustc --version; cargo --version; rustup --version - - restore_cache: - keys: - - cargocache-wasm-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + name: Install Docker client, rust/cargo libs (build-base) + command: apk add docker-cli build-base + - setup_remote_docker + - checkout - run: - name: Add wasm32 target - command: rustup target add wasm32-unknown-unknown + # We cannot mount local folders, see https://circleci.com/docs/2.0/building-docker-images/#mounting-folders + name: Prepare volume with source code + command: | + # create a dummy container which will hold a volume with config + docker create -v /code --name with_code alpine /bin/true + # copy a config file into this volume + docker cp Cargo.toml with_code:/code + docker cp Cargo.lock with_code:/code + # copy code into this volume + docker cp ./contracts with_code:/code + docker cp ./packages with_code:/code + - run: + name: Build and optimize contracts + command: | + docker run --volumes-from with_code cosmwasm/workspace-optimizer:0.16.0 + docker cp with_code:/code/artifacts ./artifacts - run: - name: Build Wasm Release + name: List artifacts and checksums command: | - for C in ./contracts/*/ - do - echo "Compiling `basename $C`..." - (cd $C && cargo build --release --target wasm32-unknown-unknown --lib --locked) - done + ls -l artifacts + cat artifacts/checksums.txt - run: - name: Install check_contract + name: Install Rust and Cargo + command: | + curl https://sh.rustup.rs -sSf | sh -s -- -y + source $HOME/.cargo/env + rustup target add wasm32-unknown-unknown + - run: + name: Install cosmwasm-check # Uses --debug for compilation speed - command: cargo install --debug --version 1.1.0 --locked -- cosmwasm-check - - save_cache: - paths: - - /usr/local/cargo/registry - - target - key: cargocache-wasm-rust:1.65.0-{{ checksum "~/project/Cargo.lock" }} + command: | + source $HOME/.cargo/env + cargo install --debug --version 1.1.0 --locked cosmwasm-check - run: name: Check wasm contracts - command: cosmwasm-check ./target/wasm32-unknown-unknown/release/*.wasm + command: | + source $HOME/.cargo/env + cosmwasm-check ./artifacts/*.wasm # This job roughly follows the instructions from https://circleci.com/blog/publishing-to-github-releases-via-circleci/ build_and_upload_contracts: @@ -410,7 +449,7 @@ jobs: - run: name: Build development contracts command: | - docker run --volumes-from with_code cosmwasm/workspace-optimizer:0.12.13 + docker run --volumes-from with_code cosmwasm/workspace-optimizer:0.16.0 docker cp with_code:/code/artifacts ./artifacts - run: name: Show data diff --git a/Cargo.lock b/Cargo.lock index 3e791817b..3f89d2064 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -15,9 +15,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.77" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "base16ct" @@ -27,9 +27,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" [[package]] name = "byteorder" @@ -75,9 +75,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cfg-if" @@ -93,9 +93,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmwasm-crypto" -version = "1.5.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" +checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" dependencies = [ "digest 0.10.7", "ecdsa", @@ -107,18 +107,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.5.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" +checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" +checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" +checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" dependencies = [ "proc-macro2", "quote", @@ -140,9 +140,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.0" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d6864742e3a7662d024b51a94ea81c9af21db6faea2f9a6d2232bb97c6e53e" +checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" dependencies = [ "base64", "bech32", @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -213,11 +213,19 @@ dependencies = [ "cosmwasm-std", ] +[[package]] +name = "cw-address-like" +version = "1.0.4" +source = "git+https://github.com/public-awesome/cw-plus-plus.git?rev=28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523#28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523" +dependencies = [ + "cosmwasm-std", +] + [[package]] name = "cw-multi-test" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fff029689ae89127cf6d7655809a68d712f3edbdb9686c70b018ba438b26ca" +checksum = "cc392a5cb7e778e3f90adbf7faa43c4db7f35b6623224b08886d796718edb875" dependencies = [ "anyhow", "bech32", @@ -225,8 +233,8 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools 0.12.0", - "prost 0.12.3", + "itertools 0.12.1", + "prost 0.12.6", "schemars", "serde", "sha2 0.10.8", @@ -241,8 +249,22 @@ checksum = "093dfb4520c48b5848274dd88ea99e280a04bc08729603341c7fb0d758c74321" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-address-like", - "cw-ownable-derive", + "cw-address-like 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-ownable-derive 0.5.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "thiserror", +] + +[[package]] +name = "cw-ownable" +version = "0.6.0" +source = "git+https://github.com/public-awesome/cw-plus-plus.git?rev=28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523#28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-address-like 1.0.4 (git+https://github.com/public-awesome/cw-plus-plus.git?rev=28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523)", + "cw-ownable-derive 0.6.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "thiserror", @@ -259,6 +281,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cw-ownable-derive" +version = "0.6.0" +source = "git+https://github.com/public-awesome/cw-plus-plus.git?rev=28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523#28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "cw-storage-plus" version = "0.16.0" @@ -313,14 +345,15 @@ dependencies = [ [[package]] name = "cw1155" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-ownable", + "cw-ownable 0.6.0", + "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", - "cw721 0.18.0", + "cw721 0.19.0", "schemars", "serde", "thiserror", @@ -328,38 +361,24 @@ dependencies = [ [[package]] name = "cw1155-base" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-ownable", + "cw-ownable 0.6.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw1155", "cw2 1.1.2", - "cw721 0.18.0", - "cw721-base 0.18.0", + "cw721 0.19.0", + "cw721-base 0.19.0", "schemars", "serde", ] -[[package]] -name = "cw1155-metadata-onchain" -version = "0.18.0" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw1155", - "cw1155-base", - "cw2 1.1.2", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw1155-royalties" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -415,13 +434,14 @@ dependencies = [ [[package]] name = "cw2981-royalties" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-ownable 0.6.0", "cw2 1.1.2", - "cw721 0.18.0", - "cw721-base 0.18.0", + "cw721 0.19.0", + "cw721-base 0.19.0", "schemars", "serde", "thiserror", @@ -440,9 +460,34 @@ dependencies = [ "serde", ] +[[package]] +name = "cw721" +version = "0.16.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.16.0#2cad1d3e15e0a34d466a0b51e02c58b82ebe5ecd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 0.16.0", + "schemars", + "serde", +] + +[[package]] +name = "cw721" +version = "0.17.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.17.0#c1ece555dded6cbcebba1d417ed2a18d47ca3c8a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "schemars", + "serde", +] + [[package]] name = "cw721" version = "0.18.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.18.0#177a993dfb5a1a3164be1baf274f43b1ca53da53" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -451,6 +496,26 @@ dependencies = [ "serde", ] +[[package]] +name = "cw721" +version = "0.19.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-ownable 0.6.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.16.0 (git+https://github.com/CosmWasm/cw-nfts?tag=v0.16.0)", + "cw721-base 0.16.0 (git+https://github.com/CosmWasm/cw-nfts?tag=v0.16.0)", + "cw721-base 0.17.0", + "cw721-base 0.18.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw721-base" version = "0.16.0" @@ -462,7 +527,7 @@ dependencies = [ "cw-storage-plus 0.16.0", "cw-utils 0.16.0", "cw2 0.16.0", - "cw721 0.16.0", + "cw721 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "schemars", "serde", "thiserror", @@ -470,92 +535,128 @@ dependencies = [ [[package]] name = "cw721-base" -version = "0.18.0" +version = "0.16.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.16.0#2cad1d3e15e0a34d466a0b51e02c58b82ebe5ecd" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-ownable", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw721 0.16.0 (git+https://github.com/CosmWasm/cw-nfts?tag=v0.16.0)", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw721-base" +version = "0.17.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.17.0#c1ece555dded6cbcebba1d417ed2a18d47ca3c8a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable 0.5.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", - "cw721 0.18.0", - "cw721-base 0.16.0", + "cw721 0.17.0", + "cw721-base 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "schemars", "serde", "thiserror", ] [[package]] -name = "cw721-expiration" +name = "cw721-base" version = "0.18.0" +source = "git+https://github.com/CosmWasm/cw-nfts?tag=v0.18.0#177a993dfb5a1a3164be1baf274f43b1ca53da53" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-ownable", + "cw-ownable 0.5.1", "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "cw2 1.1.2", "cw721 0.18.0", - "cw721-base 0.18.0", + "cw721-base 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "schemars", "serde", "thiserror", ] [[package]] -name = "cw721-fixed-price" -version = "0.18.0" +name = "cw721-base" +version = "0.19.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable 0.6.0", + "cw2 1.1.2", + "cw721 0.17.0", + "cw721 0.18.0", + "cw721 0.19.0", + "serde", +] + +[[package]] +name = "cw721-expiration" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test", + "cw-ownable 0.6.0", "cw-storage-plus 1.2.0", - "cw-utils 1.0.3", "cw2 1.1.2", - "cw20", - "cw721-base 0.18.0", - "prost 0.10.4", + "cw721 0.19.0", + "cw721-base 0.19.0", "schemars", "serde", "thiserror", ] [[package]] -name = "cw721-metadata-onchain" -version = "0.18.0" +name = "cw721-fixed-price" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "cw2 1.1.2", - "cw721 0.18.0", - "cw721-base 0.18.0", + "cw20", + "cw721 0.19.0", + "cw721-base 0.19.0", + "prost 0.10.4", "schemars", "serde", + "thiserror", ] [[package]] name = "cw721-non-transferable" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw2 1.1.2", - "cw721 0.18.0", - "cw721-base 0.18.0", + "cw721 0.19.0", + "cw721-base 0.19.0", "schemars", "serde", ] [[package]] name = "cw721-receiver-tester" -version = "0.18.0" +version = "0.19.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw721 0.18.0", - "cw721-base 0.18.0", + "cw721 0.19.0", + "cw721-base 0.19.0", "schemars", "serde", "thiserror", @@ -563,9 +664,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", @@ -605,9 +706,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -640,9 +741,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "elliptic-curve" @@ -692,9 +793,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -747,33 +848,24 @@ dependencies = [ [[package]] name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "k256" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa", @@ -785,9 +877,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "once_cell" @@ -797,9 +889,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "pkcs8" @@ -813,9 +905,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.71" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -832,12 +924,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", - "prost-derive 0.12.3", + "prost-derive 0.12.6", ] [[package]] @@ -855,22 +947,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.3" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.66", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -902,15 +994,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schemars" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", @@ -920,14 +1012,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -946,55 +1038,55 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde-json-wasm" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.66", ] [[package]] name = "serde_derive_internals" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -1070,9 +1162,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.43" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1081,22 +1173,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.52" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.52" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn 2.0.66", ] [[package]] @@ -1125,6 +1217,6 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 94031ef51..45ec8a234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,26 +2,31 @@ members = ["packages/*", "contracts/*"] [workspace.package] -version = "0.18.0" +version = "0.19.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/CosmWasm/cw-nfts" homepage = "https://cosmwasm.com" documentation = "https://docs.cosmwasm.com" -rust-version = "1.65" +rust-version = "1.78" [workspace.dependencies] -cosmwasm-schema = "^1.2" -cosmwasm-std = "^1.2" +cosmwasm-schema = "^1.5" +cosmwasm-std = "^1.5" cw2 = "^1.1" cw20 = "^1.1" cw721 = { version = "*", path = "./packages/cw721" } +cw721-016 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.16.0", package = "cw721" } # needed for backwards compatibility and legacy migration +cw721-017 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.17.0", package = "cw721" } # needed for testing legacy migration +cw721-018 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.18.0", package = "cw721" } # needed for testing legacy migration cw721-base = { version = "*", path = "./contracts/cw721-base" } -cw721-base-016 = { version = "0.16.0", package = "cw721-base" } +cw721-base-016 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.16.0", package = "cw721-base" } # needed for testing legacy migration +cw721-base-017 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.17.0", package = "cw721-base" } # needed for testing legacy migration +cw721-base-018 = { git = "https://github.com/CosmWasm/cw-nfts", tag = "v0.18.0", package = "cw721-base" } # needed for testing legacy migration cw1155 = { path = "./packages/cw1155", version = "*" } cw1155-base = { path = "./contracts/cw1155-base", version = "*" } cw-multi-test = "^0.20" -cw-ownable = "^0.5" +cw-ownable = { git = "https://github.com/public-awesome/cw-plus-plus.git", rev = "28c1a09bfc6b4f1942fefe3eb0b50faf9d3b1523"} # TODO: switch to official https://github.com/larry0x/cw-plus-plus once merged cw-storage-plus = "^1.1" cw-utils = "^1.0" schemars = "^0.8" diff --git a/Makefile.toml b/Makefile.toml index 951218c6c..633e010ed 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -32,7 +32,7 @@ fi docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - ${image}:0.15.0 + ${image}:0.16.0 """ [tasks.schema] diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..df1d9651c --- /dev/null +++ b/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Compiles and optimizes contracts + +set -o errexit -o nounset -o pipefail +command -v shellcheck >/dev/null && shellcheck "$0" + +cd "$(git rev-parse --show-toplevel)" +docker run --rm -v "$(pwd)":/code --platform linux/amd64 \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/workspace-optimizer:0.16.0 # https://hub.docker.com/r/cosmwasm/workspace-optimizer/tags +ls -al ./artifacts/*wasm diff --git a/contracts/cw1155-base/examples/schema.rs b/contracts/cw1155-base/examples/schema.rs index 5df5cc3ce..1666716a7 100644 --- a/contracts/cw1155-base/examples/schema.rs +++ b/contracts/cw1155-base/examples/schema.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::write_api; +use cw1155::msg::Cw1155InstantiateMsg; -use cw1155::Cw1155InstantiateMsg; use cw1155_base::{Cw1155BaseExecuteMsg, Cw1155BaseQueryMsg}; fn main() { diff --git a/contracts/cw1155-base/src/contract_tests.rs b/contracts/cw1155-base/src/contract_tests.rs index 41a19aa3c..1345e6f40 100644 --- a/contracts/cw1155-base/src/contract_tests.rs +++ b/contracts/cw1155-base/src/contract_tests.rs @@ -1,18 +1,24 @@ #[cfg(test)] mod tests { + use crate::{Cw1155BaseContract, Cw1155BaseExecuteMsg, Cw1155BaseQueryMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ from_json, to_json_binary, Addr, Binary, Empty, Event, OverflowError, Response, StdError, Uint128, }; - use cw_ownable::OwnershipError; - - use crate::{Cw1155BaseContract, Cw1155BaseExecuteMsg, Cw1155BaseQueryMsg}; - use cw1155::{ - ApprovedForAllResponse, Balance, BalanceResponse, BalancesResponse, Cw1155BatchReceiveMsg, - Cw1155ContractError, Cw1155InstantiateMsg, Cw1155MintMsg, Cw1155QueryMsg, Expiration, - NumTokensResponse, OwnerToken, TokenAmount, TokenInfoResponse, TokensResponse, + use cw1155::error::Cw1155ContractError; + use cw1155::execute::Cw1155Execute; + use cw1155::msg::{ + ApprovedForAllResponse, Balance, BalanceResponse, BalancesResponse, Cw1155InstantiateMsg, + Cw1155MintMsg, Cw1155QueryMsg, NumTokensResponse, OwnerToken, TokenAmount, + TokenInfoResponse, }; + use cw1155::query::Cw1155Query; + use cw1155::receiver::Cw1155BatchReceiveMsg; + use cw721::msg::TokensResponse; + use cw721::Approval; + use cw_ownable::OwnershipError; + use cw_utils::Expiration; #[test] fn check_transfers() { @@ -48,7 +54,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg) + .instantiate( + deps.as_mut(), + mock_env(), + mock_info("operator", &[]), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); @@ -444,7 +457,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), mock_env(), operator_info.clone(), msg) + .instantiate( + deps.as_mut(), + mock_env(), + operator_info.clone(), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); @@ -522,7 +542,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg) + .instantiate( + deps.as_mut(), + mock_env(), + mock_info("operator", &[]), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); @@ -722,8 +749,8 @@ mod tests { }, ), to_json_binary(&ApprovedForAllResponse { - operators: vec![cw1155::Approval { - spender: users[3].clone(), + operators: vec![Approval { + spender: Addr::unchecked(&users[3]), expires: Expiration::Never {}, }], }) @@ -751,7 +778,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg) + .instantiate( + deps.as_mut(), + env.clone(), + mock_info("operator", &[]), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); @@ -863,7 +897,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg) + .instantiate( + deps.as_mut(), + env.clone(), + mock_info("operator", &[]), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); @@ -950,7 +991,14 @@ mod tests { minter: Some(minter.to_string()), }; let res = contract - .instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg) + .instantiate( + deps.as_mut(), + mock_env(), + mock_info("operator", &[]), + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); diff --git a/contracts/cw1155-base/src/execute.rs b/contracts/cw1155-base/src/execute.rs index 8926dcef0..165181165 100644 --- a/contracts/cw1155-base/src/execute.rs +++ b/contracts/cw1155-base/src/execute.rs @@ -1,720 +1,50 @@ -use crate::state::{Cw1155Contract, TokenInfo}; -use crate::{CONTRACT_NAME, CONTRACT_VERSION}; -use cosmwasm_std::{ - Addr, BankMsg, Binary, CustomMsg, DepsMut, Env, Event, MessageInfo, Order, Response, StdResult, - Storage, SubMsg, Uint128, -}; -use cw1155::{ - ApproveAllEvent, ApproveEvent, Balance, BurnEvent, Cw1155BatchReceiveMsg, Cw1155ContractError, - Cw1155ExecuteMsg, Cw1155InstantiateMsg, Cw1155MintMsg, Cw1155ReceiveMsg, Expiration, MintEvent, - RevokeAllEvent, RevokeEvent, TokenAmount, TokenApproval, TransferEvent, -}; -use cw2::set_contract_version; +use crate::Cw1155Contract; +use cosmwasm_std::CustomMsg; +use cw1155::execute::Cw1155Execute; +use cw721::execute::Cw721Execute; use serde::de::DeserializeOwned; use serde::Serialize; -impl<'a, T, C, E, Q> Cw1155Contract<'a, T, C, E, Q> +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw1155Execute< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw1155Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - pub fn instantiate( - &self, - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: Cw1155InstantiateMsg, - ) -> StdResult> { - // store contract version - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // store contract info - let contract_info = cw721::ContractInfoResponse { - name: msg.name, - symbol: msg.symbol, - }; - self.contract_info.save(deps.storage, &contract_info)?; - - // store minter - let owner = match msg.minter { - Some(owner) => deps.api.addr_validate(&owner)?, - None => info.sender, - }; - cw_ownable::initialize_owner(deps.storage, deps.api, Some(owner.as_ref()))?; - - // store total supply - self.supply.save(deps.storage, &Uint128::zero())?; - - Ok(Response::default()) - } - - pub fn execute( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: Cw1155ExecuteMsg, - ) -> Result, Cw1155ContractError> { - let env = ExecuteEnv { deps, env, info }; - match msg { - // cw1155 - Cw1155ExecuteMsg::SendBatch { - from, - to, - batch, - msg, - } => self.send_batch(env, from, to, batch, msg), - Cw1155ExecuteMsg::MintBatch { recipient, msgs } => { - self.mint_batch(env, recipient, msgs) - } - Cw1155ExecuteMsg::BurnBatch { from, batch } => self.burn_batch(env, from, batch), - Cw1155ExecuteMsg::ApproveAll { operator, expires } => { - self.approve_all(env, operator, expires) - } - Cw1155ExecuteMsg::RevokeAll { operator } => self.revoke_all(env, operator), - - // cw721 - Cw1155ExecuteMsg::Send { - from, - to, - token_id, - amount, - msg, - } => self.send(env, from, to, token_id, amount, msg), - Cw1155ExecuteMsg::Mint { recipient, msg } => self.mint(env, recipient, msg), - Cw1155ExecuteMsg::Burn { - from, - token_id, - amount, - } => self.burn(env, from, token_id, amount), - Cw1155ExecuteMsg::Approve { - spender, - token_id, - amount, - expires, - } => self.approve_token(env, spender, token_id, amount, expires), - Cw1155ExecuteMsg::Revoke { - spender, - token_id, - amount, - } => self.revoke_token(env, spender, token_id, amount), - Cw1155ExecuteMsg::UpdateOwnership(action) => Self::update_ownership(env, action), - - Cw1155ExecuteMsg::Extension { .. } => unimplemented!(), - } - } -} - -/// To mitigate clippy::too_many_arguments warning -pub struct ExecuteEnv<'a> { - deps: DepsMut<'a>, - env: Env, - info: MessageInfo, } -// helper -impl<'a, T, C, E, Q> Cw1155Contract<'a, T, C, E, Q> +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Execute< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw1155Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TCustomResponseMessage: CustomMsg, + TMetadataExtension: Clone + DeserializeOwned + Serialize, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - pub fn mint( - &self, - env: ExecuteEnv, - recipient: String, - msg: Cw1155MintMsg, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - cw_ownable::assert_owner(deps.storage, &info.sender)?; - - let to = deps.api.addr_validate(&recipient)?; - - let mut rsp = Response::default(); - - let event = self.update_balances( - &mut deps, - &env, - None, - Some(to), - vec![TokenAmount { - token_id: msg.token_id.to_string(), - amount: msg.amount, - }], - )?; - rsp = rsp.add_event(event); - - // store token info if not exist (if it is the first mint) - if !self.tokens.has(deps.storage, &msg.token_id) { - let token_info = TokenInfo { - token_uri: msg.token_uri, - extension: msg.extension, - }; - self.tokens.save(deps.storage, &msg.token_id, &token_info)?; - } - - Ok(rsp) - } - - pub fn mint_batch( - &self, - env: ExecuteEnv, - recipient: String, - msgs: Vec>, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - cw_ownable::assert_owner(deps.storage, &info.sender)?; - - let to = deps.api.addr_validate(&recipient)?; - - let batch = msgs - .iter() - .map(|msg| { - // store token info if not exist (if it is the first mint) - if !self.tokens.has(deps.storage, &msg.token_id) { - let token_info = TokenInfo { - token_uri: msg.token_uri.clone(), - extension: msg.extension.clone(), - }; - self.tokens.save(deps.storage, &msg.token_id, &token_info)?; - } - Ok(TokenAmount { - token_id: msg.token_id.to_string(), - amount: msg.amount, - }) - }) - .collect::>>()?; - - let mut rsp = Response::default(); - let event = self.update_balances(&mut deps, &env, None, Some(to), batch)?; - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn send( - &self, - env: ExecuteEnv, - from: Option, - to: String, - token_id: String, - amount: Uint128, - msg: Option, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - env, - info, - } = env; - - let from = if let Some(from) = from { - deps.api.addr_validate(&from)? - } else { - info.sender.clone() - }; - let to = deps.api.addr_validate(&to)?; - - let balance_update = - self.verify_approval(deps.storage, &env, &info, &from, &token_id, amount)?; - - let mut rsp = Response::::default(); - - let event = self.update_balances( - &mut deps, - &env, - Some(from.clone()), - Some(to.clone()), - vec![TokenAmount { - token_id: token_id.to_string(), - amount: balance_update.amount, - }], - )?; - rsp.events.push(event); - - if let Some(msg) = msg { - rsp.messages.push(SubMsg::new( - Cw1155ReceiveMsg { - operator: info.sender.to_string(), - from: Some(from.to_string()), - amount, - token_id, - msg, - } - .into_cosmos_msg(&info, to)?, - )); - } else { - // transfer funds along to recipient - if info.funds.len() > 0 { - let transfer_msg = BankMsg::Send { - to_address: to.to_string(), - amount: info.funds.to_vec(), - }; - rsp.messages.push(SubMsg::new(transfer_msg)); - } - } - - Ok(rsp) - } - - pub fn send_batch( - &self, - env: ExecuteEnv, - from: Option, - to: String, - batch: Vec, - msg: Option, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - env, - info, - } = env; - - let from = if let Some(from) = from { - deps.api.addr_validate(&from)? - } else { - info.sender.clone() - }; - let to = deps.api.addr_validate(&to)?; - - let batch = self.verify_approvals(deps.storage, &env, &info, &from, batch)?; - - let mut rsp = Response::::default(); - let event = self.update_balances( - &mut deps, - &env, - Some(from.clone()), - Some(to.clone()), - batch.to_vec(), - )?; - rsp.events.push(event); - - if let Some(msg) = msg { - rsp.messages.push(SubMsg::new( - Cw1155BatchReceiveMsg { - operator: info.sender.to_string(), - from: Some(from.to_string()), - batch, - msg, - } - .into_cosmos_msg(&info, to)?, - )); - } else { - // transfer funds along to recipient - if info.funds.len() > 0 { - let transfer_msg = BankMsg::Send { - to_address: to.to_string(), - amount: info.funds.to_vec(), - }; - rsp.messages.push(SubMsg::new(transfer_msg)); - } - } - - Ok(rsp) - } - - pub fn burn( - &self, - env: ExecuteEnv, - from: Option, - token_id: String, - amount: Uint128, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - let from = if let Some(from) = from { - deps.api.addr_validate(&from)? - } else { - info.sender.clone() - }; - - // whoever can transfer these tokens can burn - let balance_update = - self.verify_approval(deps.storage, &env, &info, &from, &token_id, amount)?; - - let mut rsp = Response::default(); - - let event = self.update_balances( - &mut deps, - &env, - Some(from), - None, - vec![TokenAmount { - token_id, - amount: balance_update.amount, - }], - )?; - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn burn_batch( - &self, - env: ExecuteEnv, - from: Option, - batch: Vec, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - let from = if let Some(from) = from { - deps.api.addr_validate(&from)? - } else { - info.sender.clone() - }; - - let batch = self.verify_approvals(deps.storage, &env, &info, &from, batch)?; - - let mut rsp = Response::default(); - let event = self.update_balances(&mut deps, &env, Some(from), None, batch)?; - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn approve_token( - &self, - env: ExecuteEnv, - operator: String, - token_id: String, - amount: Option, - expiration: Option, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { deps, info, env } = env; - - // reject expired data as invalid - let expiration = expiration.unwrap_or_default(); - if expiration.is_expired(&env.block) { - return Err(Cw1155ContractError::Expired {}); - } - - // get sender's token balance to get valid approval amount - let balance = self - .balances - .load(deps.storage, (info.sender.clone(), token_id.to_string()))?; - let approval_amount = amount.unwrap_or(Uint128::MAX).min(balance.amount); - - // store the approval - let operator = deps.api.addr_validate(&operator)?; - self.token_approves.save( - deps.storage, - (&token_id, &info.sender, &operator), - &TokenApproval { - amount: approval_amount, - expiration, - }, - )?; - - let mut rsp = Response::default(); - - let event = ApproveEvent::new(&info.sender, &operator, &token_id, approval_amount).into(); - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn approve_all( - &self, - env: ExecuteEnv, - operator: String, - expires: Option, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { deps, info, env } = env; - - // reject expired data as invalid - let expires = expires.unwrap_or_default(); - if expires.is_expired(&env.block) { - return Err(Cw1155ContractError::Expired {}); - } - - // set the operator for us - let operator = deps.api.addr_validate(&operator)?; - self.approves - .save(deps.storage, (&info.sender, &operator), &expires)?; - - let mut rsp = Response::default(); - - let event = ApproveAllEvent::new(&info.sender, &operator).into(); - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn revoke_token( - &self, - env: ExecuteEnv, - operator: String, - token_id: String, - amount: Option, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { deps, info, .. } = env; - let operator = deps.api.addr_validate(&operator)?; - - // get prev approval amount to get valid revoke amount - let prev_approval = self - .token_approves - .load(deps.storage, (&token_id, &info.sender, &operator))?; - let revoke_amount = amount.unwrap_or(Uint128::MAX).min(prev_approval.amount); - - // remove or update approval - if revoke_amount == prev_approval.amount { - self.token_approves - .remove(deps.storage, (&token_id, &info.sender, &operator)); - } else { - self.token_approves.update( - deps.storage, - (&token_id, &info.sender, &operator), - |prev| -> StdResult<_> { - let mut new_approval = prev.unwrap(); - new_approval.amount = new_approval.amount.checked_sub(revoke_amount)?; - Ok(new_approval) - }, - )?; - } - - let mut rsp = Response::default(); - - let event = RevokeEvent::new(&info.sender, &operator, &token_id, revoke_amount).into(); - rsp = rsp.add_event(event); - - Ok(rsp) - } - - pub fn revoke_all( - &self, - env: ExecuteEnv, - operator: String, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { deps, info, .. } = env; - let operator_addr = deps.api.addr_validate(&operator)?; - self.approves - .remove(deps.storage, (&info.sender, &operator_addr)); - - let mut rsp = Response::default(); - - let event = RevokeAllEvent::new(&info.sender, &operator_addr).into(); - rsp = rsp.add_event(event); - - Ok(rsp) - } - - /// When from is None: mint new tokens - /// When to is None: burn tokens - /// When both are Some: transfer tokens - /// - /// Make sure permissions are checked before calling this. - fn update_balances( - &self, - deps: &mut DepsMut, - env: &Env, - from: Option, - to: Option, - tokens: Vec, - ) -> Result { - if let Some(from) = &from { - for TokenAmount { token_id, amount } in tokens.iter() { - self.balances.update( - deps.storage, - (from.clone(), token_id.to_string()), - |balance: Option| -> StdResult<_> { - let mut new_balance = balance.unwrap(); - new_balance.amount = new_balance.amount.checked_sub(*amount)?; - Ok(new_balance) - }, - )?; - } - } - - if let Some(to) = &to { - for TokenAmount { token_id, amount } in tokens.iter() { - self.balances.update( - deps.storage, - (to.clone(), token_id.to_string()), - |balance: Option| -> StdResult<_> { - let mut new_balance: Balance = if let Some(balance) = balance { - balance - } else { - Balance { - owner: to.clone(), - amount: Uint128::zero(), - token_id: token_id.to_string(), - } - }; - - new_balance.amount = new_balance.amount.checked_add(*amount)?; - Ok(new_balance) - }, - )?; - } - } - - let event = if let Some(from) = &from { - for TokenAmount { token_id, amount } in &tokens { - // remove token approvals - for (operator, approval) in self - .token_approves - .prefix((token_id, from)) - .range(deps.storage, None, None, Order::Ascending) - .collect::>>()? - { - if approval.is_expired(env) || approval.amount <= *amount { - self.token_approves - .remove(deps.storage, (token_id, from, &operator)); - } else { - self.token_approves.update( - deps.storage, - (token_id, from, &operator), - |prev| -> StdResult<_> { - let mut new_approval = prev.unwrap(); - new_approval.amount = new_approval.amount.checked_sub(*amount)?; - Ok(new_approval) - }, - )?; - } - } - - // decrement tokens if burning - if to.is_none() { - self.decrement_tokens(deps.storage, token_id, amount)?; - } - } - - if let Some(to) = &to { - // transfer - TransferEvent::new(from, to, tokens).into() - } else { - // burn - BurnEvent::new(from, tokens).into() - } - } else if let Some(to) = &to { - // mint - for TokenAmount { token_id, amount } in &tokens { - self.increment_tokens(deps.storage, token_id, amount)?; - } - MintEvent::new(to, tokens).into() - } else { - panic!("Invalid transfer: from and to cannot both be None") - }; - - Ok(event) - } - - /// returns valid token amount if the sender can execute or is approved to execute - pub fn verify_approval( - &self, - storage: &dyn Storage, - env: &Env, - info: &MessageInfo, - owner: &Addr, - token_id: &str, - amount: Uint128, - ) -> Result { - let operator = &info.sender; - - let owner_balance = self - .balances - .load(storage, (owner.clone(), token_id.to_string()))?; - let mut balance_update = TokenAmount { - token_id: token_id.to_string(), - amount: owner_balance.amount.min(amount), - }; - - // owner or all operator can execute - if owner == operator || self.verify_all_approval(storage, env, owner, operator) { - return Ok(balance_update); - } - - // token operator can execute up to approved amount - if let Some(token_approval) = - self.get_active_token_approval(storage, env, owner, operator, token_id) - { - balance_update.amount = balance_update.amount.min(token_approval.amount); - return Ok(balance_update); - } - - Err(Cw1155ContractError::Unauthorized {}) - } - - /// returns valid token amounts if the sender can execute or is approved to execute on all provided tokens - pub fn verify_approvals( - &self, - storage: &dyn Storage, - env: &Env, - info: &MessageInfo, - owner: &Addr, - tokens: Vec, - ) -> Result, Cw1155ContractError> { - tokens - .iter() - .map(|TokenAmount { token_id, amount }| { - self.verify_approval(storage, env, info, owner, token_id, *amount) - }) - .collect() - } - - pub fn verify_all_approval( - &self, - storage: &dyn Storage, - env: &Env, - owner: &Addr, - operator: &Addr, - ) -> bool { - match self.approves.load(storage, (owner, operator)) { - Ok(ex) => !ex.is_expired(&env.block), - Err(_) => false, - } - } - - pub fn get_active_token_approval( - &self, - storage: &dyn Storage, - env: &Env, - owner: &Addr, - operator: &Addr, - token_id: &str, - ) -> Option { - match self - .token_approves - .load(storage, (token_id, owner, operator)) - { - Ok(approval) => { - if !approval.is_expired(env) { - Some(approval) - } else { - None - } - } - Err(_) => None, - } - } - - pub fn update_ownership( - env: ExecuteEnv, - action: cw_ownable::Action, - ) -> Result, Cw1155ContractError> { - let ExecuteEnv { deps, info, env } = env; - let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; - Ok(Response::new().add_attributes(ownership.into_attributes())) - } } diff --git a/contracts/cw1155-base/src/lib.rs b/contracts/cw1155-base/src/lib.rs index 140aba97f..c84273ad4 100644 --- a/contracts/cw1155-base/src/lib.rs +++ b/contracts/cw1155-base/src/lib.rs @@ -5,19 +5,20 @@ pub mod state; pub use crate::state::Cw1155Contract; use cosmwasm_std::Empty; -use cw1155::{Cw1155ExecuteMsg, Cw1155QueryMsg}; +use cw1155::msg::{Cw1155ExecuteMsg, Cw1155QueryMsg}; +use cw1155::state::Cw1155Config; +use cw721::state::DefaultOptionMetadataExtension; // Version info for migration pub const CONTRACT_NAME: &str = "crates.io:cw1155-base"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const EXPECTED_FROM_VERSION: &str = CONTRACT_VERSION; -// This is a simple type to let us handle empty extensions -pub type Extension = Option; -pub type Cw1155BaseExecuteMsg = Cw1155ExecuteMsg; -pub type Cw1155BaseQueryMsg = Cw1155QueryMsg; - -pub type Cw1155BaseContract<'a> = Cw1155Contract<'a, Extension, Empty, Empty, Empty>; +pub type Cw1155BaseContract<'a> = + Cw1155Contract<'a, DefaultOptionMetadataExtension, Empty, Empty, Empty>; +pub type Cw1155BaseExecuteMsg = Cw1155ExecuteMsg; +pub type Cw1155BaseQueryMsg = Cw1155QueryMsg; +pub type Cw1155BaseConfig<'a> = + Cw1155Config<'a, DefaultOptionMetadataExtension, Empty, Empty, Empty>; pub mod entry { use super::*; @@ -25,7 +26,11 @@ pub mod entry { #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; - use cw1155::{Cw1155ContractError, Cw1155InstantiateMsg}; + use cw1155::error::Cw1155ContractError; + use cw1155::execute::Cw1155Execute; + use cw1155::msg::{Cw1155ExecuteMsg, Cw1155InstantiateMsg, Cw1155QueryMsg}; + use cw1155::query::Cw1155Query; + use cw721::state::DefaultOptionMetadataExtension; // This makes a conscious choice on the various generics used by the contract #[cfg_attr(not(feature = "library"), entry_point)] @@ -34,11 +39,9 @@ pub mod entry { env: Env, info: MessageInfo, msg: Cw1155InstantiateMsg, - ) -> StdResult { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - + ) -> Result { let tract = Cw1155BaseContract::default(); - tract.instantiate(deps, env, info, msg) + tract.instantiate(deps, env, info, msg, CONTRACT_NAME, CONTRACT_VERSION) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -46,27 +49,25 @@ pub mod entry { deps: DepsMut, env: Env, info: MessageInfo, - msg: Cw1155BaseExecuteMsg, + msg: Cw1155ExecuteMsg, ) -> Result { let tract = Cw1155BaseContract::default(); tract.execute(deps, env, info, msg) } #[cfg_attr(not(feature = "library"), entry_point)] - pub fn query(deps: Deps, env: Env, msg: Cw1155BaseQueryMsg) -> StdResult { + pub fn query( + deps: Deps, + env: Env, + msg: Cw1155QueryMsg, + ) -> StdResult { let tract = Cw1155BaseContract::default(); tract.query(deps, env, msg) } #[cfg_attr(not(feature = "library"), entry_point)] - pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - // make sure the correct contract is being upgraded, and it's being - // upgraded from the correct version. - cw2::assert_contract_version(deps.as_ref().storage, CONTRACT_NAME, EXPECTED_FROM_VERSION)?; - - // update contract version - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - Ok(Response::default()) + pub fn migrate(deps: DepsMut, env: Env, msg: Empty) -> Result { + let contract = Cw1155BaseContract::default(); + contract.migrate(deps, env, msg, CONTRACT_NAME, CONTRACT_VERSION) } } diff --git a/contracts/cw1155-base/src/query.rs b/contracts/cw1155-base/src/query.rs index b0c27a512..39e689d28 100644 --- a/contracts/cw1155-base/src/query.rs +++ b/contracts/cw1155-base/src/query.rs @@ -1,260 +1,50 @@ +use crate::Cw1155Contract; +use cosmwasm_std::CustomMsg; +use cw1155::query::Cw1155Query; +use cw721::query::Cw721Query; use serde::de::DeserializeOwned; use serde::Serialize; -use cosmwasm_std::{ - to_json_binary, Addr, Binary, CustomMsg, Deps, Empty, Env, Order, StdResult, Uint128, -}; - -use cw1155::{ - Approval, ApprovedForAllResponse, Balance, BalanceResponse, BalancesResponse, Cw1155QueryMsg, - Expiration, IsApprovedForAllResponse, NumTokensResponse, OwnerToken, TokenInfoResponse, - TokensResponse, -}; -use cw721_base::Cw721Contract; -use cw_storage_plus::Bound; -use cw_utils::maybe_addr; - -use crate::state::Cw1155Contract; - -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 100; - -impl<'a, T, C, E, Q> Cw1155Contract<'a, T, C, E, Q> +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw1155Query< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw1155Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - pub fn query(&self, deps: Deps, env: Env, msg: Cw1155QueryMsg) -> StdResult { - match msg { - Cw1155QueryMsg::Minter {} => { - let tract = Cw721Contract::::default(); - to_json_binary(&tract.minter(deps)?) - } - Cw1155QueryMsg::BalanceOf(OwnerToken { owner, token_id }) => { - let owner_addr = deps.api.addr_validate(&owner)?; - let balance = self - .balances - .may_load(deps.storage, (owner_addr.clone(), token_id.clone()))? - .unwrap_or(Balance { - owner: owner_addr, - token_id, - amount: Uint128::new(0), - }); - to_json_binary(&BalanceResponse { - balance: balance.amount, - }) - } - Cw1155QueryMsg::AllBalances { - token_id, - start_after, - limit, - } => to_json_binary(&self.query_all_balances(deps, token_id, start_after, limit)?), - Cw1155QueryMsg::BalanceOfBatch(batch) => { - let balances = batch - .into_iter() - .map(|OwnerToken { owner, token_id }| { - let owner = Addr::unchecked(owner); - self.balances - .load(deps.storage, (owner.clone(), token_id.to_string())) - .unwrap_or(Balance { - owner, - token_id, - amount: Uint128::zero(), - }) - }) - .collect::>(); - to_json_binary(&BalancesResponse { balances }) - } - Cw1155QueryMsg::TokenApprovals { - owner, - token_id, - include_expired, - } => { - let owner = deps.api.addr_validate(&owner)?; - let approvals = self - .token_approves - .prefix((&token_id, &owner)) - .range(deps.storage, None, None, Order::Ascending) - .filter_map(|approval| { - let (_, approval) = approval.unwrap(); - if include_expired.unwrap_or(false) || !approval.is_expired(&env) { - Some(approval) - } else { - None - } - }) - .collect::>(); - to_json_binary(&approvals) - } - Cw1155QueryMsg::ApprovalsForAll { - owner, - include_expired, - start_after, - limit, - } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let start_addr = maybe_addr(deps.api, start_after)?; - to_json_binary(&self.query_all_approvals( - deps, - env, - owner_addr, - include_expired.unwrap_or(false), - start_addr, - limit, - )?) - } - Cw1155QueryMsg::IsApprovedForAll { owner, operator } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let operator_addr = deps.api.addr_validate(&operator)?; - let approved = - self.verify_all_approval(deps.storage, &env, &owner_addr, &operator_addr); - to_json_binary(&IsApprovedForAllResponse { approved }) - } - Cw1155QueryMsg::TokenInfo { token_id } => { - let token_info = self.tokens.load(deps.storage, &token_id)?; - to_json_binary(&TokenInfoResponse:: { - token_uri: token_info.token_uri, - extension: token_info.extension, - }) - } - Cw1155QueryMsg::Tokens { - owner, - start_after, - limit, - } => { - let owner_addr = deps.api.addr_validate(&owner)?; - to_json_binary(&self.query_owner_tokens(deps, owner_addr, start_after, limit)?) - } - Cw1155QueryMsg::ContractInfo {} => { - to_json_binary(&self.contract_info.load(deps.storage)?) - } - Cw1155QueryMsg::NumTokens { token_id } => { - let count = if let Some(token_id) = token_id { - self.token_count(deps.storage, &token_id)? - } else { - self.supply.load(deps.storage)? - }; - to_json_binary(&NumTokensResponse { count }) - } - Cw1155QueryMsg::AllTokens { start_after, limit } => { - to_json_binary(&self.query_all_tokens(deps, start_after, limit)?) - } - Cw1155QueryMsg::Ownership {} => { - to_json_binary(&cw_ownable::get_ownership(deps.storage)?) - } - - Cw1155QueryMsg::Extension { .. } => { - unimplemented!() - } - } - } } -impl<'a, T, C, E, Q> Cw1155Contract<'a, T, C, E, Q> +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Query< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw1155Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TCustomResponseMessage: CustomMsg, + TMetadataExtension: Clone + DeserializeOwned + Serialize, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - fn query_all_approvals( - &self, - deps: Deps, - env: Env, - owner: Addr, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_ref().map(Bound::exclusive); - - let operators = self - .approves - .prefix(&owner) - .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(build_approval) - .collect::>()?; - Ok(ApprovedForAllResponse { operators }) - } - - fn query_owner_tokens( - &self, - deps: Deps, - owner: Addr, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); - - let tokens = self - .balances - .prefix(owner) - .keys(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>()?; - Ok(TokensResponse { tokens }) - } - - fn query_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.as_ref().map(|s| Bound::exclusive(s.as_str())); - let tokens = self - .tokens - .keys(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>()?; - Ok(TokensResponse { tokens }) - } - - fn query_all_balances( - &self, - deps: Deps, - token_id: String, - start_after: Option, - limit: Option, - ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - - let start = if let Some(start_after) = start_after { - let start_key = (Addr::unchecked(start_after), token_id.clone()); - Some(Bound::exclusive::<(Addr, String)>(start_key)) - } else { - None - }; - - let balances: Vec = self - .balances - .idx - .token_id - .prefix(token_id) - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - let (_, v) = item.unwrap(); - v - }) - .collect(); - - Ok(BalancesResponse { balances }) - } -} - -fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { - item.map(|(addr, expires)| Approval { - spender: addr.into(), - expires, - }) } diff --git a/contracts/cw1155-base/src/state.rs b/contracts/cw1155-base/src/state.rs index fca601eae..89734b62a 100644 --- a/contracts/cw1155-base/src/state.rs +++ b/contracts/cw1155-base/src/state.rs @@ -1,157 +1,48 @@ -use cosmwasm_schema::cw_serde; +use cosmwasm_std::CustomMsg; +use cw1155::state::Cw1155Config; use serde::de::DeserializeOwned; use serde::Serialize; -use std::marker::PhantomData; -use cosmwasm_std::{Addr, CustomMsg, StdError, StdResult, Storage, Uint128}; - -use cw1155::{Balance, Expiration, TokenApproval}; -use cw721::ContractInfoResponse; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; - -pub struct Cw1155Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - E: CustomMsg, - Q: CustomMsg, +pub struct Cw1155Contract< + 'a, + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - pub contract_info: Item<'a, ContractInfoResponse>, - pub supply: Item<'a, Uint128>, // total supply of all tokens - // key: token id - pub token_count: Map<'a, &'a str, Uint128>, // total supply of a specific token - // key: (owner, token id) - pub balances: IndexedMap<'a, (Addr, String), Balance, BalanceIndexes<'a>>, - // key: (owner, spender) - pub approves: Map<'a, (&'a Addr, &'a Addr), Expiration>, - // key: (token id, owner, spender) - pub token_approves: Map<'a, (&'a str, &'a Addr, &'a Addr), TokenApproval>, - // key: token id - pub tokens: Map<'a, &'a str, TokenInfo>, - - pub(crate) _custom_response: PhantomData, - pub(crate) _custom_query: PhantomData, - pub(crate) _custom_execute: PhantomData, + pub config: Cw1155Config< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >, } -impl<'a, T, C, E, Q> Default for Cw1155Contract<'a, T, C, E, Q> +impl Default + for Cw1155Contract< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { fn default() -> Self { - Self::new( - "cw1155_contract_info", - "tokens", - "token_count", - "supply", - "balances", - "balances__token_id", - "approves", - "token_approves", - ) - } -} - -impl<'a, T, C, E, Q> Cw1155Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, -{ - #[allow(clippy::too_many_arguments)] - fn new( - contract_info_key: &'a str, - tokens_key: &'a str, - token_count_key: &'a str, - supply_key: &'a str, - balances_key: &'a str, - balances_token_id_key: &'a str, - approves_key: &'a str, - token_approves_key: &'a str, - ) -> Self { - let balances_indexes = BalanceIndexes { - token_id: MultiIndex::new( - |_, b| b.token_id.to_string(), - balances_key, - balances_token_id_key, - ), - }; Self { - contract_info: Item::new(contract_info_key), - tokens: Map::new(tokens_key), - token_count: Map::new(token_count_key), - supply: Item::new(supply_key), - balances: IndexedMap::new(balances_key, balances_indexes), - approves: Map::new(approves_key), - token_approves: Map::new(token_approves_key), - _custom_execute: PhantomData, - _custom_query: PhantomData, - _custom_response: PhantomData, + config: Cw1155Config::default(), } } - - pub fn token_count(&self, storage: &dyn Storage, token_id: &'a str) -> StdResult { - Ok(self - .token_count - .may_load(storage, token_id)? - .unwrap_or_default()) - } - - pub fn increment_tokens( - &self, - storage: &mut dyn Storage, - token_id: &'a str, - amount: &Uint128, - ) -> StdResult { - // increment token count - let val = self.token_count(storage, token_id)? + amount; - self.token_count.save(storage, token_id, &val)?; - - // increment total supply - self.supply.update(storage, |prev| { - Ok::(prev.checked_add(*amount)?) - })?; - - Ok(val) - } - - pub fn decrement_tokens( - &self, - storage: &mut dyn Storage, - token_id: &'a str, - amount: &Uint128, - ) -> StdResult { - // decrement token count - let val = self.token_count(storage, token_id)?.checked_sub(*amount)?; - self.token_count.save(storage, token_id, &val)?; - - // decrement total supply - self.supply.update(storage, |prev| { - Ok::(prev.checked_sub(*amount)?) - })?; - - Ok(val) - } -} - -#[cw_serde] -pub struct TokenInfo { - /// Metadata JSON Schema - pub token_uri: Option, - /// You can add any custom metadata here when you extend cw1155-base - pub extension: T, -} - -pub struct BalanceIndexes<'a> { - pub token_id: MultiIndex<'a, String, Balance, (Addr, String)>, -} - -impl<'a> IndexList for BalanceIndexes<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.token_id]; - Box::new(v.into_iter()) - } } diff --git a/contracts/cw1155-metadata-onchain/.cargo/config b/contracts/cw1155-metadata-onchain/.cargo/config deleted file mode 100644 index 7d1a066c8..000000000 --- a/contracts/cw1155-metadata-onchain/.cargo/config +++ /dev/null @@ -1,5 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -wasm-debug = "build --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example schema" diff --git a/contracts/cw1155-metadata-onchain/Cargo.toml b/contracts/cw1155-metadata-onchain/Cargo.toml deleted file mode 100644 index b31f6901b..000000000 --- a/contracts/cw1155-metadata-onchain/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cw1155-metadata-onchain" -authors = ["shab "] -description = "Example extending CW1155 Token to store metadata on chain" -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } - -exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "artifacts/*", -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[dependencies] -cw2 = { workspace = true } -cw1155 = { workspace = true } -cw1155-base = { workspace = true, features = ["library"] } -cosmwasm-std = { workspace = true } -schemars = { workspace = true } -serde = { workspace = true } -thiserror = { workspace = true } -cosmwasm-schema = { workspace = true } - diff --git a/contracts/cw1155-metadata-onchain/README.md b/contracts/cw1155-metadata-onchain/README.md deleted file mode 100644 index fcc7d2e7a..000000000 --- a/contracts/cw1155-metadata-onchain/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# CW1155 Metadata Onchain - -Token creators may want to store their token metadata on-chain so other contracts are able to interact with it. -With CW1155-Base in CosmWasm, we allow you to store any data on chain you wish, using a generic `extension: T`. - -In order to support on-chain metadata, and to demonstrate how to use the extension ability, we have created this simple contract. -There is no business logic here, but looking at `lib.rs` will show you how do define custom data that is included when minting and -available in all queries. - -In particular, here we define: - -```rust -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct Trait { - pub display_type: Option, - pub trait_type: String, - pub value: String, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct Metadata { - pub image: Option, - pub image_data: Option, - pub external_url: Option, - pub description: Option, - pub name: Option, - pub attributes: Option>, - pub background_color: Option, - pub animation_url: Option, - pub youtube_url: Option, -} - -pub type Extension = Option; -``` - -In particular, the fields defined conform to the properties supported in the [OpenSea Metadata Standard](https://docs.opensea.io/docs/metadata-standards). - - -This means when you query `NftInfo{name: "Enterprise"}`, you will get something like: - -```json -{ - "name": "Enterprise", - "token_uri": "https://starships.example.com/Starship/Enterprise.json", - "extension": { - "image": null, - "image_data": null, - "external_url": null, - "description": "Spaceship with Warp Drive", - "name": "Starship USS Enterprise", - "attributes": null, - "background_color": null, - "animation_url": null, - "youtube_url": null - } -} -``` - -Please look at the test code for an example usage in Rust. - -## Notice - -Feel free to use this contract out of the box, or as inspiration for further customization of cw1155-base. -We will not be adding new features or business logic here. diff --git a/contracts/cw1155-metadata-onchain/examples/schema.rs b/contracts/cw1155-metadata-onchain/examples/schema.rs deleted file mode 100644 index 5f0fa8ddd..000000000 --- a/contracts/cw1155-metadata-onchain/examples/schema.rs +++ /dev/null @@ -1,13 +0,0 @@ -use cosmwasm_schema::write_api; -use cosmwasm_std::Empty; - -use cw1155::{Cw1155InstantiateMsg, Cw1155QueryMsg}; -use cw1155_metadata_onchain::Cw1155MetadataExecuteMsg; - -fn main() { - write_api! { - instantiate: Cw1155InstantiateMsg, - execute: Cw1155MetadataExecuteMsg, - query: Cw1155QueryMsg, - } -} diff --git a/contracts/cw1155-metadata-onchain/src/lib.rs b/contracts/cw1155-metadata-onchain/src/lib.rs deleted file mode 100644 index 7faa47104..000000000 --- a/contracts/cw1155-metadata-onchain/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::Empty; - -use cw1155::Cw1155ExecuteMsg; -pub use cw1155::{Cw1155ContractError, Cw1155InstantiateMsg, Cw1155MintMsg}; -use cw2::set_contract_version; - -// Version info for migration -const CONTRACT_NAME: &str = "crates.io:cw1155-metadata-onchain"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cw_serde] -pub struct Trait { - pub display_type: Option, - pub trait_type: String, - pub value: String, -} - -// see: https://docs.opensea.io/docs/metadata-standards -#[cw_serde] -#[derive(Default)] -pub struct Metadata { - pub image: Option, - pub image_data: Option, - pub external_url: Option, - pub description: Option, - pub name: Option, - pub attributes: Option>, - pub background_color: Option, - pub animation_url: Option, - pub youtube_url: Option, -} - -pub type Extension = Option; - -pub type Cw1155MetadataContract<'a> = - cw1155_base::Cw1155Contract<'a, Extension, Empty, Empty, Empty>; -pub type Cw1155MetadataExecuteMsg = Cw1155ExecuteMsg; - -#[cfg(not(feature = "library"))] -pub mod entry { - use super::*; - - use cosmwasm_std::entry_point; - use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; - use cw1155::Cw1155QueryMsg; - - // This makes a conscious choice on the various generics used by the contract - #[entry_point] - pub fn instantiate( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - msg: Cw1155InstantiateMsg, - ) -> Result { - let res = Cw1155MetadataContract::default().instantiate(deps.branch(), env, info, msg)?; - // Explicitly set contract name and version, otherwise set to cw1155-base info - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION) - .map_err(Cw1155ContractError::Std)?; - Ok(res) - } - - #[entry_point] - pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: Cw1155MetadataExecuteMsg, - ) -> Result { - Cw1155MetadataContract::default().execute(deps, env, info, msg) - } - - #[entry_point] - pub fn query(deps: Deps, env: Env, msg: Cw1155QueryMsg) -> StdResult { - Cw1155MetadataContract::default().query(deps, env, msg) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_json, Uint128}; - - const CREATOR: &str = "creator"; - - #[test] - fn use_metadata_extension() { - let mut deps = mock_dependencies(); - let contract = Cw1155MetadataContract::default(); - - let info = mock_info(CREATOR, &[]); - let init_msg = Cw1155InstantiateMsg { - name: "name".to_string(), - symbol: "symbol".to_string(), - minter: Some(CREATOR.to_string()), - }; - contract - .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg) - .unwrap(); - - let token_id = "Enterprise"; - let mint_msg = Cw1155MintMsg { - token_id: token_id.to_string(), - amount: Uint128::new(1), - token_uri: Some("https://starships.example.com/Starship/Enterprise.json".into()), - extension: Some(Metadata { - description: Some("Spaceship with Warp Drive".into()), - name: Some("Starship USS Enterprise".to_string()), - ..Metadata::default() - }), - }; - let exec_msg = Cw1155MetadataExecuteMsg::Mint { - recipient: "john".to_string(), - msg: mint_msg.clone(), - }; - contract - .execute(deps.as_mut(), mock_env(), info, exec_msg) - .unwrap(); - - let res: cw1155::TokenInfoResponse = from_json( - contract - .query( - deps.as_ref(), - mock_env(), - cw1155::Cw1155QueryMsg::TokenInfo { - token_id: token_id.to_string(), - }, - ) - .unwrap(), - ) - .unwrap(); - - assert_eq!(res.token_uri, mint_msg.token_uri); - assert_eq!(res.extension, mint_msg.extension); - } -} diff --git a/contracts/cw1155-royalties/examples/schema.rs b/contracts/cw1155-royalties/examples/schema.rs index 087a49ebb..6b7a4aa60 100644 --- a/contracts/cw1155-royalties/examples/schema.rs +++ b/contracts/cw1155-royalties/examples/schema.rs @@ -1,6 +1,5 @@ use cosmwasm_schema::write_api; - -use cw1155::Cw1155InstantiateMsg; +use cw1155::msg::Cw1155InstantiateMsg; use cw1155_royalties::{Cw1155RoyaltiesExecuteMsg, Cw1155RoyaltiesQueryMsg}; fn main() { diff --git a/contracts/cw1155-royalties/src/error.rs b/contracts/cw1155-royalties/src/error.rs index 9099c9771..457f46411 100644 --- a/contracts/cw1155-royalties/src/error.rs +++ b/contracts/cw1155-royalties/src/error.rs @@ -1,5 +1,5 @@ use cosmwasm_std::StdError; -use cw1155::Cw1155ContractError; +use cw1155::error::Cw1155ContractError; use thiserror::Error; #[derive(Debug, Error, PartialEq)] diff --git a/contracts/cw1155-royalties/src/lib.rs b/contracts/cw1155-royalties/src/lib.rs index 1553c97c3..9245212bb 100644 --- a/contracts/cw1155-royalties/src/lib.rs +++ b/contracts/cw1155-royalties/src/lib.rs @@ -1,9 +1,8 @@ use cosmwasm_std::Empty; -use cw1155::{Cw1155ExecuteMsg, Cw1155QueryMsg}; -pub use cw1155::{Cw1155InstantiateMsg, Cw1155MintMsg}; +use cw1155::msg::{Cw1155ExecuteMsg, Cw1155QueryMsg}; +use cw1155::state::Cw1155Config; use cw1155_base::Cw1155Contract; -use cw2::set_contract_version; -use cw2981_royalties::msg::Cw2981QueryMsg; +use cw2981_royalties::msg::QueryMsg as Cw2981QueryMsg; use cw2981_royalties::Extension; mod query; @@ -18,7 +17,8 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); pub type Cw1155RoyaltiesContract<'a> = Cw1155Contract<'a, Extension, Empty, Empty, Cw2981QueryMsg>; pub type Cw1155RoyaltiesExecuteMsg = Cw1155ExecuteMsg; -pub type Cw1155RoyaltiesQueryMsg = Cw1155QueryMsg; +pub type Cw1155RoyaltiesQueryMsg = Cw1155QueryMsg; +pub type Cw1155RoyaltiesConfig<'a> = Cw1155Config<'a, Extension, Empty, Empty, Cw2981QueryMsg>; #[cfg(not(feature = "library"))] pub mod entry { @@ -26,7 +26,9 @@ pub mod entry { use cosmwasm_std::{entry_point, to_json_binary}; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; - use cw2981_royalties::msg::Cw2981QueryMsg; + use cw1155::execute::Cw1155Execute; + use cw1155::query::Cw1155Query; + use cw2981_royalties::msg::QueryMsg as Cw2981QueryMsg; use cw2981_royalties::{check_royalties, Metadata}; // This makes a conscious choice on the various generics used by the contract @@ -35,13 +37,18 @@ pub mod entry { mut deps: DepsMut, env: Env, info: MessageInfo, - msg: Cw1155InstantiateMsg, + msg: cw1155::msg::Cw1155InstantiateMsg, ) -> Result { - let res = Cw1155RoyaltiesContract::default().instantiate(deps.branch(), env, info, msg)?; - // Explicitly set contract name and version, otherwise set to cw1155-base info - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION) - .map_err(Cw1155RoyaltiesContractError::Std)?; - Ok(res) + Cw1155RoyaltiesContract::default() + .instantiate( + deps.branch(), + env, + info, + msg, + CONTRACT_NAME, + CONTRACT_VERSION, + ) + .map_err(Into::into) } #[entry_point] @@ -53,7 +60,7 @@ pub mod entry { ) -> Result { if let Cw1155RoyaltiesExecuteMsg::Mint { msg: - Cw1155MintMsg { + cw1155::msg::Cw1155MintMsg { extension: Some(Metadata { royalty_percentage: Some(royalty_percentage), @@ -84,12 +91,13 @@ pub mod entry { #[entry_point] pub fn query(deps: Deps, env: Env, msg: Cw1155RoyaltiesQueryMsg) -> StdResult { match msg { - Cw1155RoyaltiesQueryMsg::Extension { msg: ext_msg } => match ext_msg { + Cw1155RoyaltiesQueryMsg::Extension { msg: ext_msg, .. } => match ext_msg { Cw2981QueryMsg::RoyaltyInfo { token_id, sale_price, } => to_json_binary(&query_royalties_info(deps, token_id, sale_price)?), Cw2981QueryMsg::CheckRoyalties {} => to_json_binary(&check_royalties(deps)?), + _ => unimplemented!(), }, _ => Cw1155RoyaltiesContract::default().query(deps, env, msg), } @@ -103,6 +111,7 @@ mod tests { use cosmwasm_std::{from_json, Uint128}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cw1155::msg::{Cw1155InstantiateMsg, Cw1155MintMsg}; use cw2981_royalties::msg::{CheckRoyaltiesResponse, RoyaltiesInfoResponse}; use cw2981_royalties::{check_royalties, Metadata}; @@ -111,7 +120,7 @@ mod tests { #[test] fn use_metadata_extension() { let mut deps = mock_dependencies(); - let contract = Cw1155RoyaltiesContract::default(); + let config = Cw1155RoyaltiesConfig::default(); let info = mock_info(CREATOR, &[]); let init_msg = Cw1155InstantiateMsg { @@ -139,7 +148,7 @@ mod tests { }; entry::execute(deps.as_mut(), mock_env(), info, exec_msg).unwrap(); - let res = contract.tokens.load(&deps.storage, token_id).unwrap(); + let res = config.tokens.load(&deps.storage, token_id).unwrap(); assert_eq!(res.token_uri, token_uri); assert_eq!(res.extension, extension); } @@ -215,6 +224,7 @@ mod tests { // also check the longhand way let query_msg = Cw1155RoyaltiesQueryMsg::Extension { msg: Cw2981QueryMsg::CheckRoyalties {}, + phantom: None, }; let query_res: CheckRoyaltiesResponse = from_json(entry::query(deps.as_ref(), mock_env(), query_msg).unwrap()).unwrap(); @@ -266,6 +276,7 @@ mod tests { token_id: token_id.to_string(), sale_price: Uint128::new(100), }, + phantom: None, }; let query_res: RoyaltiesInfoResponse = from_json(entry::query(deps.as_ref(), mock_env(), query_msg).unwrap()).unwrap(); diff --git a/contracts/cw1155-royalties/src/query.rs b/contracts/cw1155-royalties/src/query.rs index dfd90a300..17e44e58a 100644 --- a/contracts/cw1155-royalties/src/query.rs +++ b/contracts/cw1155-royalties/src/query.rs @@ -1,4 +1,4 @@ -use crate::Cw1155RoyaltiesContract; +use crate::Cw1155RoyaltiesConfig; use cosmwasm_std::{Decimal, Deps, StdResult, Uint128}; use cw2981_royalties::msg::RoyaltiesInfoResponse; @@ -10,8 +10,8 @@ pub fn query_royalties_info( token_id: String, sale_price: Uint128, ) -> StdResult { - let contract = Cw1155RoyaltiesContract::default(); - let token_info = contract.tokens.load(deps.storage, &token_id)?; + let config = Cw1155RoyaltiesConfig::default(); + let token_info = config.tokens.load(deps.storage, &token_id)?; let royalty_percentage = match token_info.extension { Some(ref ext) => match ext.royalty_percentage { diff --git a/contracts/cw2981-royalties/Cargo.toml b/contracts/cw2981-royalties/Cargo.toml index d9140bc37..e66cb6284 100644 --- a/contracts/cw2981-royalties/Cargo.toml +++ b/contracts/cw2981-royalties/Cargo.toml @@ -21,6 +21,7 @@ library = [] [dependencies] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } +cw-ownable = { workspace = true } cw2 = { workspace = true } cw721 = { workspace = true } cw721-base = { workspace = true, features = ["library"] } diff --git a/contracts/cw2981-royalties/examples/schema.rs b/contracts/cw2981-royalties/examples/schema.rs index f3da971f8..055a210ad 100644 --- a/contracts/cw2981-royalties/examples/schema.rs +++ b/contracts/cw2981-royalties/examples/schema.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::write_api; -use cw2981_royalties::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cw2981_royalties::{msg::QueryMsg, ExecuteMsg, InstantiateMsg}; fn main() { write_api! { diff --git a/contracts/cw2981-royalties/schema/cw2981-royalties.json b/contracts/cw2981-royalties/schema/cw2981-royalties.json index 5ac8e2923..3040363db 100644 --- a/contracts/cw2981-royalties/schema/cw2981-royalties.json +++ b/contracts/cw2981-royalties/schema/cw2981-royalties.json @@ -1,6 +1,6 @@ { "contract_name": "cw2981-royalties", - "contract_version": "0.18.0", + "contract_version": "0.19.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -38,8 +38,19 @@ "execute": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", - "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", "oneOf": [ + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, { "description": "Transfer is a base message to move a token to another account without triggering actions", "type": "object", @@ -359,19 +370,6 @@ } }, "additionalProperties": false - }, - { - "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", - "type": "object", - "required": [ - "update_ownership" - ], - "properties": { - "update_ownership": { - "$ref": "#/definitions/Action" - } - }, - "additionalProperties": false } ], "definitions": { @@ -619,6 +617,46 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", "oneOf": [ + { + "description": "Should be called on sale to see if royalties are owed by the marketplace selling the NFT, if CheckRoyalties returns true See https://eips.ethereum.org/EIPS/eip-2981", + "type": "object", + "required": [ + "royalty_info" + ], + "properties": { + "royalty_info": { + "type": "object", + "required": [ + "sale_price", + "token_id" + ], + "properties": { + "sale_price": { + "$ref": "#/definitions/Uint128" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Called against contract to determine if this NFT implements royalties. Should return a boolean as part of CheckRoyaltiesResponse - default can simply be true if royalties are implemented at token level (i.e. always check on sale)", + "type": "object", + "required": [ + "check_royalties" + ], + "properties": { + "check_royalties": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Return the owner of the given token, error if token does not exist", "type": "object", @@ -798,7 +836,6 @@ "additionalProperties": false }, { - "description": "With MetaData Extension. Returns top-level metadata about the contract", "type": "object", "required": [ "contract_info" @@ -811,6 +848,33 @@ }, "additionalProperties": false }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract", + "type": "object", + "required": [ + "get_collection_info" + ], + "properties": { + "get_collection_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", "type": "object", @@ -942,28 +1006,6 @@ }, "additionalProperties": false }, - { - "description": "Extension query", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Cw2981QueryMsg" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -978,14 +1020,25 @@ "additionalProperties": false }, { - "description": "Query the contract's ownership information", "type": "object", "required": [ - "ownership" + "extension" ], "properties": { - "ownership": { + "extension": { "type": "object", + "properties": { + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + } + }, "additionalProperties": false } }, @@ -993,49 +1046,106 @@ } ], "definitions": { - "Cw2981QueryMsg": { - "oneOf": [ - { - "description": "Should be called on sale to see if royalties are owed by the marketplace selling the NFT, if CheckRoyalties returns true See https://eips.ethereum.org/EIPS/eip-2981", - "type": "object", - "required": [ - "royalty_info" + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" ], - "properties": { - "royalty_info": { - "type": "object", - "required": [ - "sale_price", - "token_id" - ], - "properties": { - "sale_price": { - "$ref": "#/definitions/Uint128" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "items": { + "$ref": "#/definitions/Trait" + } }, - { - "description": "Called against contract to determine if this NFT implements royalties. Should return a boolean as part of CheckRoyaltiesResponse - default can simply be true if royalties are implemented at token level (i.e. always check on sale)", - "type": "object", - "required": [ - "check_royalties" + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "royalty_payment_address": { + "description": "The payment address, may be different to or the same as the minter addr question: how do we validate this?", + "type": [ + "string", + "null" + ] + }, + "royalty_percentage": { + "description": "This is how much the minter takes as a cut when sold royalties are owed on this token if it is Some", + "type": [ + "integer", + "null" ], - "properties": { - "check_royalties": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + "format": "uint64", + "minimum": 0.0 + }, + "youtube_url": { + "type": [ + "string", + "null" + ] } - ] + }, + "additionalProperties": false + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -1048,7 +1158,7 @@ "responses": { "all_nft_info": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllNftInfoResponse_for_Cw2981QueryMsg", + "title": "AllNftInfoResponse_for_Nullable_Metadata", "type": "object", "required": [ "access", @@ -1067,13 +1177,17 @@ "description": "Data on the token itself,", "allOf": [ { - "$ref": "#/definitions/NftInfoResponse_for_Cw2981QueryMsg" + "$ref": "#/definitions/NftInfoResponse_for_Nullable_Metadata" } ] } }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1091,63 +1205,23 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false }, - "Cw2981QueryMsg": { + "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": "Should be called on sale to see if royalties are owed by the marketplace selling the NFT, if CheckRoyalties returns true See https://eips.ethereum.org/EIPS/eip-2981", + "description": "AtHeight will expire when `env.block.height` >= height", "type": "object", "required": [ - "royalty_info" - ], - "properties": { - "royalty_info": { - "type": "object", - "required": [ - "sale_price", - "token_id" - ], - "properties": { - "sale_price": { - "$ref": "#/definitions/Uint128" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Called against contract to determine if this NFT implements royalties. Should return a boolean as part of CheckRoyaltiesResponse - default can simply be true if royalties are implemented at token level (i.e. always check on sale)", - "type": "object", - "required": [ - "check_royalties" - ], - "properties": { - "check_royalties": { - "type": "object", - "additionalProperties": false - } - }, - "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" + "at_height" ], "properties": { "at_height": { @@ -1187,17 +1261,96 @@ } ] }, - "NftInfoResponse_for_Cw2981QueryMsg": { + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "royalty_payment_address": { + "description": "The payment address, may be different to or the same as the minter addr question: how do we validate this?", + "type": [ + "string", + "null" + ] + }, + "royalty_percentage": { + "description": "This is how much the minter takes as a cut when sold royalties are owed on this token if it is Some", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "NftInfoResponse_for_Nullable_Metadata": { "type": "object", - "required": [ - "extension" - ], "properties": { "extension": { "description": "You can add any custom metadata here when you extend cw721-base", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, { - "$ref": "#/definitions/Cw2981QueryMsg" + "type": "null" } ] }, @@ -1240,9 +1393,27 @@ } ] }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false }, "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); ```", @@ -1267,6 +1438,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1284,7 +1459,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false @@ -1382,6 +1561,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1399,7 +1582,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false @@ -1482,6 +1669,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1499,7 +1690,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false @@ -1565,9 +1760,24 @@ } } }, + "check_royalties": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CheckRoyaltiesResponse", + "description": "Shows if the contract implements royalties if royalty_payments is true, marketplaces should pay them", + "type": "object", + "required": [ + "royalty_payments" + ], + "properties": { + "royalty_payments": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "contract_info": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ContractInfoResponse", + "title": "CollectionInfo", "type": "object", "required": [ "name", @@ -1588,6 +1798,24 @@ "title": "Null", "type": "null" }, + "get_collection_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CollectionInfo", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "additionalProperties": false + }, "get_withdraw_address": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Nullable_String", @@ -1599,7 +1827,7 @@ "minter": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MinterResponse", - "description": "Shows who can mint these tokens", + "description": "Deprecated: use Cw721QueryMsg::GetMinterOwnership instead! Shows who can mint these tokens.", "type": "object", "properties": { "minter": { @@ -1613,17 +1841,17 @@ }, "nft_info": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "NftInfoResponse_for_Cw2981QueryMsg", + "title": "NftInfoResponse_for_Nullable_Metadata", "type": "object", - "required": [ - "extension" - ], "properties": { "extension": { "description": "You can add any custom metadata here when you extend cw721-base", - "allOf": [ + "anyOf": [ { - "$ref": "#/definitions/Cw2981QueryMsg" + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" } ] }, @@ -1637,53 +1865,106 @@ }, "additionalProperties": false, "definitions": { - "Cw2981QueryMsg": { - "oneOf": [ - { - "description": "Should be called on sale to see if royalties are owed by the marketplace selling the NFT, if CheckRoyalties returns true See https://eips.ethereum.org/EIPS/eip-2981", - "type": "object", - "required": [ - "royalty_info" + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" ], - "properties": { - "royalty_info": { - "type": "object", - "required": [ - "sale_price", - "token_id" - ], - "properties": { - "sale_price": { - "$ref": "#/definitions/Uint128" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "items": { + "$ref": "#/definitions/Trait" + } }, - { - "description": "Called against contract to determine if this NFT implements royalties. Should return a boolean as part of CheckRoyaltiesResponse - default can simply be true if royalties are implemented at token level (i.e. always check on sale)", - "type": "object", - "required": [ - "check_royalties" + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "royalty_payment_address": { + "description": "The payment address, may be different to or the same as the minter addr question: how do we validate this?", + "type": [ + "string", + "null" + ] + }, + "royalty_percentage": { + "description": "This is how much the minter takes as a cut when sold royalties are owed on this token if it is Some", + "type": [ + "integer", + "null" ], - "properties": { - "check_royalties": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false + "format": "uint64", + "minimum": 0.0 + }, + "youtube_url": { + "type": [ + "string", + "null" + ] } - ] + }, + "additionalProperties": false }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -1717,6 +1998,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1734,7 +2019,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false @@ -1823,6 +2112,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -1840,7 +2133,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false @@ -1908,15 +2205,19 @@ }, "ownership": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Ownership_for_String", + "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.", - "type": [ - "string", - "null" + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } ] }, "pending_expiry": { @@ -1932,14 +2233,22 @@ }, "pending_owner": { "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", - "type": [ - "string", - "null" + "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": [ @@ -2001,6 +2310,30 @@ } } }, + "royalty_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RoyaltiesInfoResponse", + "type": "object", + "required": [ + "address", + "royalty_amount" + ], + "properties": { + "address": { + "type": "string" + }, + "royalty_amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "tokens": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TokensResponse", diff --git a/contracts/cw2981-royalties/src/error.rs b/contracts/cw2981-royalties/src/error.rs index 73c0e4bb4..2d1cd6324 100644 --- a/contracts/cw2981-royalties/src/error.rs +++ b/contracts/cw2981-royalties/src/error.rs @@ -7,7 +7,7 @@ pub enum ContractError { Std(#[from] StdError), #[error(transparent)] - Base(#[from] cw721_base::ContractError), + Base(#[from] cw721_base::error::ContractError), #[error("Royalty percentage must be between 0 and 100")] InvalidRoyaltyPercentage, diff --git a/contracts/cw2981-royalties/src/lib.rs b/contracts/cw2981-royalties/src/lib.rs index 42edc0caf..ad229e815 100644 --- a/contracts/cw2981-royalties/src/lib.rs +++ b/contracts/cw2981-royalties/src/lib.rs @@ -6,15 +6,15 @@ pub use query::{check_royalties, query_royalties_info}; use cosmwasm_schema::cw_serde; use cosmwasm_std::Empty; -use cw721_base::Cw721Contract; -pub use cw721_base::InstantiateMsg; +pub use cw721_base::{ + execute::Cw721Execute, msg::InstantiateMsg, query::Cw721Query, Cw721Contract, +}; -use crate::msg::Cw2981QueryMsg; +use crate::error::ContractError; +use crate::msg::QueryMsg; // Version info for migration -#[cfg(not(feature = "library"))] const CONTRACT_NAME: &str = "crates.io:cw2981-royalties"; -#[cfg(not(feature = "library"))] const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cw_serde] @@ -50,16 +50,16 @@ pub type Extension = Option; pub type MintExtension = Option; -pub type Cw2981Contract<'a> = Cw721Contract<'a, Extension, Empty, Empty, Cw2981QueryMsg>; -pub type ExecuteMsg = cw721_base::ExecuteMsg; -pub type QueryMsg = cw721_base::QueryMsg; +pub type Cw2981Contract<'a> = Cw721Contract<'a, Extension, Empty, Empty, QueryMsg>; +pub type ExecuteMsg = cw721_base::msg::ExecuteMsg; #[cfg(not(feature = "library"))] pub mod entry { + use self::msg::QueryMsg; + use super::*; - use crate::error::ContractError; - use cosmwasm_std::{entry_point, to_json_binary}; + use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; #[entry_point] @@ -69,9 +69,14 @@ pub mod entry { info: MessageInfo, msg: InstantiateMsg, ) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - Ok(Cw2981Contract::default().instantiate(deps.branch(), env, info, msg)?) + Ok(Cw2981Contract::default().instantiate( + deps.branch(), + env, + info, + msg, + CONTRACT_NAME, + CONTRACT_VERSION, + )?) } #[entry_point] @@ -105,29 +110,25 @@ pub mod entry { #[entry_point] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Extension { msg } => match msg { - Cw2981QueryMsg::RoyaltyInfo { - token_id, - sale_price, - } => to_json_binary(&query_royalties_info(deps, token_id, sale_price)?), - Cw2981QueryMsg::CheckRoyalties {} => to_json_binary(&check_royalties(deps)?), - }, - _ => Cw2981Contract::default().query(deps, env, msg), + QueryMsg::RoyaltyInfo { + token_id, + sale_price, + } => to_json_binary(&query_royalties_info(deps, env, token_id, sale_price)?), + QueryMsg::CheckRoyalties {} => to_json_binary(&check_royalties(deps)?), + _ => Cw2981Contract::default().query(deps, env, msg.into()), } } } -#[cfg(test)] #[cfg(not(feature = "library"))] +#[cfg(test)] mod tests { use super::*; - use crate::msg::{CheckRoyaltiesResponse, RoyaltiesInfoResponse}; + use crate::msg::{CheckRoyaltiesResponse, QueryMsg, RoyaltiesInfoResponse}; use cosmwasm_std::{from_json, Uint128}; - use crate::error::ContractError; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cw721::Cw721Query; const CREATOR: &str = "creator"; @@ -158,9 +159,12 @@ mod tests { token_uri: token_uri.clone(), extension: extension.clone(), }; - entry::execute(deps.as_mut(), mock_env(), info, exec_msg).unwrap(); + let env = mock_env(); + entry::execute(deps.as_mut(), env.clone(), info, exec_msg).unwrap(); - let res = contract.nft_info(deps.as_ref(), token_id.into()).unwrap(); + let res = contract + .query_nft_info(deps.as_ref(), env, token_id.into()) + .unwrap(); assert_eq!(res.token_uri, token_uri); assert_eq!(res.extension, extension); } @@ -230,9 +234,7 @@ mod tests { assert_eq!(res, expected); // also check the longhand way - let query_msg = QueryMsg::Extension { - msg: Cw2981QueryMsg::CheckRoyalties {}, - }; + let query_msg = QueryMsg::CheckRoyalties {}; let query_res: CheckRoyaltiesResponse = from_json(entry::query(deps.as_ref(), mock_env(), query_msg).unwrap()).unwrap(); assert_eq!(query_res, expected); @@ -249,7 +251,8 @@ mod tests { minter: None, withdraw_address: None, }; - entry::instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg).unwrap(); + let env = mock_env(); + entry::instantiate(deps.as_mut(), env.clone(), info.clone(), init_msg).unwrap(); let token_id = "Enterprise"; let owner = "jeanluc"; @@ -271,16 +274,19 @@ mod tests { address: owner.into(), royalty_amount: Uint128::new(10), }; - let res = - query_royalties_info(deps.as_ref(), token_id.to_string(), Uint128::new(100)).unwrap(); + let res = query_royalties_info( + deps.as_ref(), + env.clone(), + token_id.to_string(), + Uint128::new(100), + ) + .unwrap(); assert_eq!(res, expected); // also check the longhand way - let query_msg = QueryMsg::Extension { - msg: Cw2981QueryMsg::RoyaltyInfo { - token_id: token_id.to_string(), - sale_price: Uint128::new(100), - }, + let query_msg = QueryMsg::RoyaltyInfo { + token_id: token_id.to_string(), + sale_price: Uint128::new(100), }; let query_res: RoyaltiesInfoResponse = from_json(entry::query(deps.as_ref(), mock_env(), query_msg).unwrap()).unwrap(); @@ -313,6 +319,7 @@ mod tests { let res = query_royalties_info( deps.as_ref(), + env, voyager_token_id.to_string(), Uint128::new(43), ) diff --git a/contracts/cw2981-royalties/src/msg.rs b/contracts/cw2981-royalties/src/msg.rs index fa9c7f387..30e3cc702 100644 --- a/contracts/cw2981-royalties/src/msg.rs +++ b/contracts/cw2981-royalties/src/msg.rs @@ -1,9 +1,19 @@ +use crate::Extension; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{CustomMsg, Uint128}; +use cosmwasm_std::{Addr, Uint128}; +use cw721::msg::Cw721QueryMsg; +use cw721_base::{ + msg::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, MinterResponse, NftInfoResponse, + NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, + }, + state::CollectionInfo, +}; +use cw_ownable::Ownership; #[cw_serde] #[derive(QueryResponses)] -pub enum Cw2981QueryMsg { +pub enum QueryMsg { /// Should be called on sale to see if royalties are owed /// by the marketplace selling the NFT, if CheckRoyalties /// returns true @@ -24,16 +34,171 @@ pub enum Cw2981QueryMsg { /// (i.e. always check on sale) #[returns(CheckRoyaltiesResponse)] CheckRoyalties {}, + + // -- below copied from Cw721QueryMsg + /// Return the owner of the given token, error if token does not exist + #[returns(OwnerOfResponse)] + OwnerOf { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + /// Return operator that can access all of the owner's tokens. + #[returns(ApprovalResponse)] + Approval { + token_id: String, + spender: String, + include_expired: Option, + }, + /// Return approvals that a token has + #[returns(ApprovalsResponse)] + Approvals { + token_id: String, + include_expired: Option, + }, + /// Return approval of a given operator for all tokens of an owner, error if not set + #[returns(OperatorResponse)] + Operator { + owner: String, + operator: String, + include_expired: Option, + }, + /// List all operators that can access all of the owner's tokens + #[returns(OperatorsResponse)] + AllOperators { + owner: String, + /// unset or false will filter out expired items, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + #[returns(NumTokensResponse)] + NumTokens {}, + + #[returns(CollectionInfo)] + ContractInfo {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract + #[returns(CollectionInfo)] + GetCollectionInfo {}, + + #[returns(Ownership)] + Ownership {}, + + /// With MetaData Extension. + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract + #[returns(NftInfoResponse)] + NftInfo { token_id: String }, + /// With MetaData Extension. + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients + #[returns(AllNftInfoResponse)] + AllNftInfo { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + #[returns(TokensResponse)] + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + #[returns(TokensResponse)] + AllTokens { + start_after: Option, + limit: Option, + }, + + /// Return the minter + #[returns(MinterResponse)] + Minter {}, + + #[returns(Option)] + GetWithdrawAddress {}, + + // -- below queries, Extension and GetCollectionInfoExtension, are just dummies, since type annotations are required for + // -- TMetadataExtension and TCollectionInfoExtension, Error: + // -- "type annotations needed: cannot infer type for type parameter `TMetadataExtension` declared on the enum `Cw721QueryMsg`" + #[returns(())] + Extension { msg: Extension }, } -impl Default for Cw2981QueryMsg { - fn default() -> Self { - Cw2981QueryMsg::CheckRoyalties {} +impl From for Cw721QueryMsg { + fn from(msg: QueryMsg) -> Cw721QueryMsg { + match msg { + QueryMsg::OwnerOf { + token_id, + include_expired, + } => Cw721QueryMsg::OwnerOf { + token_id, + include_expired, + }, + QueryMsg::NumTokens {} => Cw721QueryMsg::NumTokens {}, + QueryMsg::ContractInfo {} => Cw721QueryMsg::ContractInfo {}, + QueryMsg::NftInfo { token_id } => Cw721QueryMsg::NftInfo { token_id }, + QueryMsg::AllNftInfo { + token_id, + include_expired, + } => Cw721QueryMsg::AllNftInfo { + token_id, + include_expired, + }, + QueryMsg::Tokens { + owner, + start_after, + limit, + } => Cw721QueryMsg::Tokens { + owner, + start_after, + limit, + }, + QueryMsg::AllTokens { start_after, limit } => { + Cw721QueryMsg::AllTokens { start_after, limit } + } + #[allow(deprecated)] + QueryMsg::Minter {} => Cw721QueryMsg::Minter {}, + QueryMsg::GetWithdrawAddress {} => Cw721QueryMsg::GetWithdrawAddress {}, + QueryMsg::AllOperators { + owner, + include_expired, + start_after, + limit, + } => Cw721QueryMsg::AllOperators { + owner, + include_expired, + start_after, + limit, + }, + QueryMsg::Approval { + token_id, + spender, + include_expired, + } => Cw721QueryMsg::Approval { + token_id, + spender, + include_expired, + }, + QueryMsg::Approvals { + token_id, + include_expired, + } => Cw721QueryMsg::Approvals { + token_id, + include_expired, + }, + msg => unreachable!("Unsupported query: {:?}", msg), + } } } -impl CustomMsg for Cw2981QueryMsg {} - #[cw_serde] pub struct RoyaltiesInfoResponse { pub address: String, diff --git a/contracts/cw2981-royalties/src/query.rs b/contracts/cw2981-royalties/src/query.rs index 53c20589f..33704e3dc 100644 --- a/contracts/cw2981-royalties/src/query.rs +++ b/contracts/cw2981-royalties/src/query.rs @@ -1,16 +1,19 @@ use crate::msg::{CheckRoyaltiesResponse, RoyaltiesInfoResponse}; -use crate::Cw2981Contract; -use cosmwasm_std::{Decimal, Deps, StdResult, Uint128}; +use crate::{Cw2981Contract, Extension}; +use cosmwasm_std::{Decimal, Deps, Env, StdResult, Uint128}; +use cw721::msg::NftInfoResponse; +use cw721_base::query::Cw721Query; /// NOTE: default behaviour here is to round down /// EIP2981 specifies that the rounding behaviour is at the discretion of the implementer pub fn query_royalties_info( deps: Deps, + env: Env, token_id: String, sale_price: Uint128, ) -> StdResult { let contract = Cw2981Contract::default(); - let token_info = contract.tokens.load(deps.storage, &token_id)?; + let token_info: NftInfoResponse = contract.query_nft_info(deps, env, token_id)?; let royalty_percentage = match token_info.extension { Some(ref ext) => match ext.royalty_percentage { diff --git a/contracts/cw721-base/Cargo.toml b/contracts/cw721-base/Cargo.toml index be71889b1..8b0427498 100644 --- a/contracts/cw721-base/Cargo.toml +++ b/contracts/cw721-base/Cargo.toml @@ -2,6 +2,7 @@ name = "cw721-base" description = "Basic implementation cw721 NFTs" authors = [ + "mr-t ", "Ethan Frey ", "Orkun Külçe ", ] @@ -26,14 +27,10 @@ library = [] 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 } -cw721-base-016 = { workspace = true, features = ["library"] } -schemars = { workspace = true } serde = { workspace = true } -thiserror = { workspace = true } [dev-dependencies] -cw-multi-test = { workspace = true } +cw721-017 = { workspace = true } +cw721-018 = { workspace = true } diff --git a/contracts/cw721-base/README.md b/contracts/cw721-base/README.md index d3be359b9..6ab157023 100644 --- a/contracts/cw721-base/README.md +++ b/contracts/cw721-base/README.md @@ -32,7 +32,7 @@ If provided, it is expected that the _token_uri_ points to a JSON file following ## Running this contract -You will need Rust 1.65+ with `wasm32-unknown-unknown` target installed. +You will need Rust 1.78+ with `wasm32-unknown-unknown` target installed. You can run unit tests on this via: diff --git a/contracts/cw721-base/examples/schema.rs b/contracts/cw721-base/examples/schema.rs index 390811894..ab49e8a4b 100644 --- a/contracts/cw721-base/examples/schema.rs +++ b/contracts/cw721-base/examples/schema.rs @@ -1,12 +1,23 @@ -use cosmwasm_schema::write_api; +use cosmwasm_schema::{export_schema_with_title, remove_schemas, schema_for}; use cosmwasm_std::Empty; - -use cw721_base::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cw721::state::DefaultOptionMetadataExtension; +use cw721_base::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use std::env::current_dir; +use std::fs::create_dir_all; fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + // entry points - generate always with title for avoiding name suffixes like "..._empty_for_..." due to generics + export_schema_with_title(&schema_for!(InstantiateMsg), &out_dir, "InstantiateMsg"); + export_schema_with_title( + &schema_for!(ExecuteMsg::), + &out_dir, + "ExecuteMsg", + ); + export_schema_with_title(&schema_for!(QueryMsg), &out_dir, "QueryMsg"); + export_schema_with_title(&schema_for!(MigrateMsg), &out_dir, "MigrateMsg"); } diff --git a/contracts/cw721-base/schema/cw721-base.json b/contracts/cw721-base/schema/cw721-base.json deleted file mode 100644 index 5b2403b29..000000000 --- a/contracts/cw721-base/schema/cw721-base.json +++ /dev/null @@ -1,1788 +0,0 @@ -{ - "contract_name": "cw721-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": [ - "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", - "null" - ] - }, - "name": { - "description": "Name of the NFT contract", - "type": "string" - }, - "symbol": { - "description": "Symbol of the NFT contract", - "type": "string" - }, - "withdraw_address": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", - "oneOf": [ - { - "description": "Transfer is a base message to move a token to another account without triggering actions", - "type": "object", - "required": [ - "transfer_nft" - ], - "properties": { - "transfer_nft": { - "type": "object", - "required": [ - "recipient", - "token_id" - ], - "properties": { - "recipient": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted Approval", - "type": "object", - "required": [ - "revoke" - ], - "properties": { - "revoke": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted ApproveAll permission", - "type": "object", - "required": [ - "revoke_all" - ], - "properties": { - "revoke_all": { - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Mint a new NFT, can only be called by the contract minter", - "type": "object", - "required": [ - "mint" - ], - "properties": { - "mint": { - "type": "object", - "required": [ - "extension", - "owner", - "token_id" - ], - "properties": { - "extension": { - "description": "Any custom extension used by this contract", - "allOf": [ - { - "$ref": "#/definitions/Empty" - } - ] - }, - "owner": { - "description": "The owner of the newly minter NFT", - "type": "string" - }, - "token_id": { - "description": "Unique ID of the NFT", - "type": "string" - }, - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Burn an NFT the sender has access to", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension msg", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets address to send withdrawn fees to. Only owner can call this.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", - "type": "object", - "required": [ - "remove_withdraw_address" - ], - "properties": { - "remove_withdraw_address": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", - "type": "object", - "required": [ - "withdraw_funds" - ], - "properties": { - "withdraw_funds": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", - "type": "object", - "required": [ - "update_ownership" - ], - "properties": { - "update_ownership": { - "$ref": "#/definitions/Action" - } - }, - "additionalProperties": false - } - ], - "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" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "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 - } - ] - }, - "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" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "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", - "oneOf": [ - { - "description": "Return the owner of the given token, error if token does not exist", - "type": "object", - "required": [ - "owner_of" - ], - "properties": { - "owner_of": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return operator that can access all of the owner's tokens.", - "type": "object", - "required": [ - "approval" - ], - "properties": { - "approval": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approvals that a token has", - "type": "object", - "required": [ - "approvals" - ], - "properties": { - "approvals": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approval of a given operator for all tokens of an owner, error if not set", - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "object", - "required": [ - "operator", - "owner" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "operator": { - "type": "string" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "List all operators that can access all of the owner's tokens", - "type": "object", - "required": [ - "all_operators" - ], - "properties": { - "all_operators": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired items, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Total number of tokens issued", - "type": "object", - "required": [ - "num_tokens" - ], - "properties": { - "num_tokens": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns top-level metadata about the contract", - "type": "object", - "required": [ - "contract_info" - ], - "properties": { - "contract_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", - "type": "object", - "required": [ - "nft_info" - ], - "properties": { - "nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", - "type": "object", - "required": [ - "all_nft_info" - ], - "properties": { - "all_nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Return the minter", - "type": "object", - "required": [ - "minter" - ], - "properties": { - "minter": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension query", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "get_withdraw_address" - ], - "properties": { - "get_withdraw_address": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query the contract's ownership information", - "type": "object", - "required": [ - "ownership" - ], - "properties": { - "ownership": { - "type": "object", - "additionalProperties": false - } - }, - "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" - } - } - }, - "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_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" - } - } - }, - "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 - }, - "extension": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Null", - "type": "null" - }, - "get_withdraw_address": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_String", - "type": [ - "string", - "null" - ] - }, - "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" - } - } - }, - "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_String", - "description": "The contract's ownership info", - "type": "object", - "properties": { - "owner": { - "description": "The contract's current owner. `None` if the ownership has been renounced.", - "type": [ - "string", - "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.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "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-base/schema/execute_msg.json b/contracts/cw721-base/schema/execute_msg.json new file mode 100644 index 000000000..2bcf2fea7 --- /dev/null +++ b/contracts/cw721-base/schema/execute_msg.json @@ -0,0 +1,562 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets address to send withdrawn fees to. Only owner can call this.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", + "type": "object", + "required": [ + "remove_withdraw_address" + ], + "properties": { + "remove_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", + "type": "object", + "required": [ + "withdraw_funds" + ], + "properties": { + "withdraw_funds": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "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" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "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 + } + ] + }, + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "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" + } + ] + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "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" + } + } +} diff --git a/contracts/cw721-base/schema/instantiate_msg.json b/contracts/cw721-base/schema/instantiate_msg.json new file mode 100644 index 000000000..1a02553e0 --- /dev/null +++ b/contracts/cw721-base/schema/instantiate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "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", + "null" + ] + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + }, + "withdraw_address": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/cw721-base/schema/migrate_msg.json b/contracts/cw721-base/schema/migrate_msg.json new file mode 100644 index 000000000..f83fa9508 --- /dev/null +++ b/contracts/cw721-base/schema/migrate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "with_update" + ], + "properties": { + "with_update": { + "type": "object", + "properties": { + "creator": { + "type": [ + "string", + "null" + ] + }, + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/cw721-base/schema/query_msg.json b/contracts/cw721-base/schema/query_msg.json new file mode 100644 index 000000000..a56350cc7 --- /dev/null +++ b/contracts/cw721-base/schema/query_msg.json @@ -0,0 +1,382 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens.", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approval of a given operator for all tokens of an owner, error if not set", + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "operator", + "owner" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Return the minter", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_withdraw_address" + ], + "properties": { + "get_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Do not use - dummy extension query, needed for inferring type parameter during compile", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "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" + } + } +} diff --git a/contracts/cw721-base/src/error.rs b/contracts/cw721-base/src/error.rs index 8ce92f08c..302102d4b 100644 --- a/contracts/cw721-base/src/error.rs +++ b/contracts/cw721-base/src/error.rs @@ -1,27 +1,2 @@ -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 }, - - #[error("No withdraw address set")] - NoWithdrawAddress {}, -} +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::error::{Cw721ContractError as ContractError, *}; diff --git a/contracts/cw721-base/src/execute.rs b/contracts/cw721-base/src/execute.rs index 246919bc4..333dad9e9 100644 --- a/contracts/cw721-base/src/execute.rs +++ b/contracts/cw721-base/src/execute.rs @@ -1,483 +1,29 @@ -use cw_ownable::OwnershipError; +use cosmwasm_std::CustomMsg; +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::execute::*; use serde::de::DeserializeOwned; use serde::Serialize; -use cosmwasm_std::{ - Addr, BankMsg, Binary, Coin, CustomMsg, Deps, DepsMut, Env, MessageInfo, Response, Storage, -}; - -use cw721::{ContractInfoResponse, Cw721Execute, Cw721ReceiveMsg, Expiration}; - -use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg}; -use crate::state::{Approval, Cw721Contract, TokenInfo}; - -impl<'a, T, C, E, Q> Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, -{ - pub fn instantiate( - &self, - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: InstantiateMsg, - ) -> Result, ContractError> { - let contract_info = ContractInfoResponse { - name: msg.name, - symbol: msg.symbol, - }; - self.contract_info.save(deps.storage, &contract_info)?; - - let owner = match msg.minter { - Some(owner) => deps.api.addr_validate(&owner)?, - None => info.sender, - }; - cw_ownable::initialize_owner(deps.storage, deps.api, Some(owner.as_ref()))?; - - if let Some(address) = msg.withdraw_address { - self.set_withdraw_address(deps, &owner, address)?; - } - - Ok(Response::default()) - } - - pub fn execute( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, - ) -> Result, ContractError> { - match msg { - ExecuteMsg::Mint { - token_id, - owner, - token_uri, - extension, - } => self.mint(deps, info, token_id, owner, token_uri, extension), - ExecuteMsg::Approve { - spender, - token_id, - expires, - } => self.approve(deps, env, info, spender, token_id, expires), - ExecuteMsg::Revoke { spender, token_id } => { - self.revoke(deps, env, info, spender, token_id) - } - ExecuteMsg::ApproveAll { operator, expires } => { - self.approve_all(deps, env, info, operator, expires) - } - ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator), - ExecuteMsg::TransferNft { - recipient, - token_id, - } => self.transfer_nft(deps, env, info, recipient, token_id), - ExecuteMsg::SendNft { - contract, - token_id, - msg, - } => self.send_nft(deps, env, info, contract, token_id, msg), - ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id), - ExecuteMsg::UpdateOwnership(action) => Self::update_ownership(deps, env, info, action), - ExecuteMsg::Extension { msg: _ } => Ok(Response::default()), - ExecuteMsg::SetWithdrawAddress { address } => { - self.set_withdraw_address(deps, &info.sender, address) - } - ExecuteMsg::RemoveWithdrawAddress {} => { - self.remove_withdraw_address(deps.storage, &info.sender) - } - ExecuteMsg::WithdrawFunds { amount } => self.withdraw_funds(deps.storage, &amount), - } - } -} - -// TODO pull this into some sort of trait extension?? -impl<'a, T, C, E, Q> Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, -{ - 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())) - } - - pub fn set_withdraw_address( - &self, - deps: DepsMut, - sender: &Addr, - address: String, - ) -> Result, ContractError> { - cw_ownable::assert_owner(deps.storage, sender)?; - deps.api.addr_validate(&address)?; - self.withdraw_address.save(deps.storage, &address)?; - Ok(Response::new() - .add_attribute("action", "set_withdraw_address") - .add_attribute("address", address)) - } - - pub fn remove_withdraw_address( - &self, - storage: &mut dyn Storage, - sender: &Addr, - ) -> Result, ContractError> { - cw_ownable::assert_owner(storage, sender)?; - let address = self.withdraw_address.may_load(storage)?; - match address { - Some(address) => { - self.withdraw_address.remove(storage); - Ok(Response::new() - .add_attribute("action", "remove_withdraw_address") - .add_attribute("address", address)) - } - None => Err(ContractError::NoWithdrawAddress {}), - } - } - - pub fn withdraw_funds( - &self, - storage: &mut dyn Storage, - amount: &Coin, - ) -> Result, ContractError> { - let address = self.withdraw_address.may_load(storage)?; - match address { - Some(address) => { - let msg = BankMsg::Send { - to_address: address, - amount: vec![amount.clone()], - }; - Ok(Response::new() - .add_message(msg) - .add_attribute("action", "withdraw_funds") - .add_attribute("amount", amount.amount.to_string()) - .add_attribute("denom", amount.denom.to_string())) - } - None => Err(ContractError::NoWithdrawAddress {}), - } - } -} - -impl<'a, T, C, E, Q> Cw721Execute for Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, -{ - type Err = ContractError; - - 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)) - } -} - -// helpers -impl<'a, T, C, E, Q> Cw721Contract<'a, T, C, E, Q> +use crate::Cw721Contract; + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Execute< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - 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)), - } - } } diff --git a/contracts/cw721-base/src/lib.rs b/contracts/cw721-base/src/lib.rs index 40f88a8a0..1d0328f29 100644 --- a/contracts/cw721-base/src/lib.rs +++ b/contracts/cw721-base/src/lib.rs @@ -1,18 +1,9 @@ pub mod error; -mod execute; -pub mod helpers; +pub mod execute; pub mod msg; -mod query; +pub mod query; pub mod state; -pub mod upgrades; -#[cfg(test)] -mod contract_tests; -#[cfg(test)] -mod multi_tests; - -pub use crate::error::ContractError; -pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; pub use crate::state::Cw721Contract; // These types are re-exported so that contracts interacting with this @@ -25,25 +16,24 @@ pub use cw_ownable::{Action, Ownership, OwnershipError}; use cosmwasm_std::Empty; -// This is a simple type to let us handle empty extensions -pub type Extension = Option; - // Version info for migration pub const CONTRACT_NAME: &str = "crates.io:cw721-base"; pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -// currently we only support migrating from 0.16.0. this is ok for now because -// we have not released any 0.16.x where x != 0 -// -// TODO: parse semvar so that any version 0.16.x can be migrated from -pub const EXPECTED_FROM_VERSION: &str = "0.16.0"; - pub mod entry { + use super::*; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + use cw721::{ + error::Cw721ContractError, + execute::Cw721Execute, + msg::{Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721MigrateMsg, Cw721QueryMsg}, + query::Cw721Query, + state::DefaultOptionMetadataExtension, + }; // This makes a conscious choice on the various generics used by the contract #[cfg_attr(not(feature = "library"), entry_point)] @@ -51,12 +41,11 @@ pub mod entry { deps: DepsMut, env: Env, info: MessageInfo, - msg: InstantiateMsg, - ) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - let tract = Cw721Contract::::default(); - tract.instantiate(deps, env, info, msg) + msg: Cw721InstantiateMsg, + ) -> Result { + let contract = + Cw721Contract::::default(); + contract.instantiate(deps, env, info, msg, CONTRACT_NAME, CONTRACT_VERSION) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -64,94 +53,32 @@ pub mod entry { deps: DepsMut, env: Env, info: MessageInfo, - msg: ExecuteMsg, - ) -> Result { - let tract = Cw721Contract::::default(); - tract.execute(deps, env, info, msg) + msg: Cw721ExecuteMsg, + ) -> Result { + let contract = + Cw721Contract::::default(); + contract.execute(deps, env, info, msg) } #[cfg_attr(not(feature = "library"), entry_point)] - pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - let tract = Cw721Contract::::default(); - tract.query(deps, env, msg) + pub fn query( + deps: Deps, + env: Env, + msg: Cw721QueryMsg, + ) -> StdResult { + let contract = + Cw721Contract::::default(); + contract.query(deps, env, msg) } #[cfg_attr(not(feature = "library"), entry_point)] - pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result { - // make sure the correct contract is being upgraded, and it's being - // upgraded from the correct version. - cw2::assert_contract_version(deps.as_ref().storage, CONTRACT_NAME, EXPECTED_FROM_VERSION)?; - - // update contract version - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // perform the upgrade - upgrades::v0_17::migrate::(deps) - } -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cw2::ContractVersion; - - use super::*; - - /// Make sure cw2 version info is properly initialized during instantiation. - #[test] - fn proper_cw2_initialization() { - let mut deps = mock_dependencies(); - - entry::instantiate( - deps.as_mut(), - mock_env(), - mock_info("larry", &[]), - InstantiateMsg { - name: "".into(), - symbol: "".into(), - minter: Some("other".into()), - withdraw_address: None, - }, - ) - .unwrap(); - - let minter = cw_ownable::get_ownership(deps.as_ref().storage) - .unwrap() - .owner - .map(|a| a.into_string()); - assert_eq!(minter, Some("other".to_string())); - - let version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); - assert_eq!( - version, - ContractVersion { - contract: CONTRACT_NAME.into(), - version: CONTRACT_VERSION.into(), - }, - ); - } - - #[test] - fn proper_owner_initialization() { - let mut deps = mock_dependencies(); - - entry::instantiate( - deps.as_mut(), - mock_env(), - mock_info("owner", &[]), - InstantiateMsg { - name: "".into(), - symbol: "".into(), - minter: None, - withdraw_address: None, - }, - ) - .unwrap(); - - let minter = cw_ownable::get_ownership(deps.as_ref().storage) - .unwrap() - .owner - .map(|a| a.into_string()); - assert_eq!(minter, Some("owner".to_string())); + pub fn migrate( + deps: DepsMut, + env: Env, + msg: Cw721MigrateMsg, + ) -> Result { + let contract = + Cw721Contract::::default(); + contract.migrate(deps, env, msg, CONTRACT_NAME, CONTRACT_VERSION) } } diff --git a/contracts/cw721-base/src/msg.rs b/contracts/cw721-base/src/msg.rs index 0ef443376..7cfb7a5c1 100644 --- a/contracts/cw721-base/src/msg.rs +++ b/contracts/cw721-base/src/msg.rs @@ -1,173 +1,5 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Binary, Coin}; -use cw721::Expiration; -use cw_ownable::{cw_ownable_execute, cw_ownable_query}; -use schemars::JsonSchema; - -#[cw_serde] -pub struct InstantiateMsg { - /// 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: Option, - - pub withdraw_address: Option, -} - -/// This is like Cw721ExecuteMsg but we add a Mint command for an owner -/// to make this stand-alone. You will likely want to remove mint and -/// use other control logic in any contract that inherits this. -#[cw_ownable_execute] -#[cw_serde] -pub enum ExecuteMsg { - /// Transfer is a base message to move a token to another account without triggering actions - TransferNft { recipient: String, token_id: String }, - /// Send is a base message to transfer a token to a contract and trigger an action - /// on the receiving contract. - SendNft { - contract: String, - token_id: String, - msg: Binary, - }, - /// Allows operator to transfer / send the token from the owner's account. - /// If expiration is set, then this allowance has a time/height limit - Approve { - spender: String, - token_id: String, - expires: Option, - }, - /// Remove previously granted Approval - Revoke { spender: String, token_id: String }, - /// Allows operator to transfer / send any token from the owner's account. - /// If expiration is set, then this allowance has a time/height limit - ApproveAll { - operator: String, - expires: Option, - }, - /// Remove previously granted ApproveAll permission - RevokeAll { operator: String }, - - /// Mint a new NFT, can only be called by the contract minter - Mint { - /// Unique ID of the NFT - token_id: String, - /// The owner of the newly minter NFT - owner: String, - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - token_uri: Option, - /// Any custom extension used by this contract - extension: T, - }, - - /// Burn an NFT the sender has access to - Burn { token_id: String }, - - /// Extension msg - Extension { msg: E }, - - /// Sets address to send withdrawn fees to. Only owner can call this. - SetWithdrawAddress { address: String }, - /// Removes the withdraw address, so fees are sent to the contract. Only owner can call this. - RemoveWithdrawAddress {}, - /// Withdraw from the contract to the given address. Anyone can call this, - /// which is okay since withdraw address has been set by owner. - WithdrawFunds { amount: Coin }, -} - -#[cw_ownable_query] -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - /// Return the owner of the given token, error if token does not exist - #[returns(cw721::OwnerOfResponse)] - OwnerOf { - token_id: String, - /// unset or false will filter out expired approvals, you must set to true to see them - include_expired: Option, - }, - /// Return operator that can access all of the owner's tokens. - #[returns(cw721::ApprovalResponse)] - Approval { - token_id: String, - spender: String, - include_expired: Option, - }, - /// Return approvals that a token has - #[returns(cw721::ApprovalsResponse)] - Approvals { - token_id: String, - include_expired: Option, - }, - /// Return approval of a given operator for all tokens of an owner, error if not set - #[returns(cw721::OperatorResponse)] - Operator { - owner: String, - operator: String, - include_expired: Option, - }, - /// List all operators that can access all of the owner's tokens - #[returns(cw721::OperatorsResponse)] - AllOperators { - owner: String, - /// unset or false will filter out expired items, you must set to true to see them - include_expired: Option, - start_after: Option, - limit: Option, - }, - /// Total number of tokens issued - #[returns(cw721::NumTokensResponse)] - NumTokens {}, - - /// With MetaData Extension. - /// Returns top-level metadata about the contract - #[returns(cw721::ContractInfoResponse)] - ContractInfo {}, - /// With MetaData Extension. - /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* - /// but directly from the contract - #[returns(cw721::NftInfoResponse)] - NftInfo { token_id: String }, - /// With MetaData Extension. - /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization - /// for clients - #[returns(cw721::AllNftInfoResponse)] - AllNftInfo { - token_id: String, - /// unset or false will filter out expired approvals, you must set to true to see them - include_expired: Option, - }, - - /// With Enumerable extension. - /// Returns all tokens owned by the given address, [] if unset. - #[returns(cw721::TokensResponse)] - Tokens { - owner: String, - start_after: Option, - limit: Option, - }, - /// With Enumerable extension. - /// Requires pagination. Lists all token_ids controlled by the contract. - #[returns(cw721::TokensResponse)] - AllTokens { - start_after: Option, - limit: Option, - }, - - /// Return the minter - #[returns(cw721::MinterResponse)] - Minter {}, - - /// Extension query - #[returns(())] - Extension { msg: Q }, - - #[returns(Option)] - GetWithdrawAddress {}, -} +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::msg::{ + Cw721ExecuteMsg as ExecuteMsg, Cw721InstantiateMsg as InstantiateMsg, + Cw721MigrateMsg as MigrateMsg, Cw721QueryMsg as QueryMsg, *, +}; diff --git a/contracts/cw721-base/src/multi_tests.rs b/contracts/cw721-base/src/multi_tests.rs deleted file mode 100644 index 345750907..000000000 --- a/contracts/cw721-base/src/multi_tests.rs +++ /dev/null @@ -1,166 +0,0 @@ -use cosmwasm_std::{to_json_binary, Addr, Empty, QuerierWrapper, WasmMsg}; -use cw721::{MinterResponse, OwnerOfResponse}; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; - -fn cw721_base_latest_contract() -> Box> { - let contract = ContractWrapper::new( - crate::entry::execute, - crate::entry::instantiate, - crate::entry::query, - ) - .with_migrate(crate::entry::migrate); - Box::new(contract) -} - -fn cw721_base_016_contract() -> Box> { - use cw721_base_016 as v16; - let contract = ContractWrapper::new( - v16::entry::execute, - v16::entry::instantiate, - v16::entry::query, - ); - Box::new(contract) -} - -fn query_owner(querier: QuerierWrapper, cw721: &Addr, token_id: String) -> Addr { - let resp: OwnerOfResponse = querier - .query_wasm_smart( - cw721, - &crate::QueryMsg::::OwnerOf { - token_id, - include_expired: None, - }, - ) - .unwrap(); - Addr::unchecked(resp.owner) -} - -fn mint_transfer_and_burn(app: &mut App, cw721: Addr, sender: Addr, token_id: String) { - app.execute_contract( - sender.clone(), - cw721.clone(), - &crate::ExecuteMsg::::Mint { - token_id: token_id.clone(), - owner: sender.to_string(), - token_uri: None, - extension: Empty::default(), - }, - &[], - ) - .unwrap(); - - let owner = query_owner(app.wrap(), &cw721, token_id.clone()); - assert_eq!(owner, sender.to_string()); - - app.execute_contract( - sender, - cw721.clone(), - &crate::ExecuteMsg::::TransferNft { - recipient: "burner".to_string(), - token_id: token_id.clone(), - }, - &[], - ) - .unwrap(); - - let owner = query_owner(app.wrap(), &cw721, token_id.clone()); - assert_eq!(owner, "burner".to_string()); - - app.execute_contract( - Addr::unchecked("burner"), - cw721, - &crate::ExecuteMsg::::Burn { token_id }, - &[], - ) - .unwrap(); -} - -/// Instantiates a 0.16 version of this contract and tests that tokens -/// can be minted, transferred, and burnred after migration. -#[test] -fn test_migration_016_to_latest() { - use cw721_base_016 as v16; - let mut app = App::default(); - let admin = || Addr::unchecked("admin"); - - let code_id_016 = app.store_code(cw721_base_016_contract()); - let code_id_latest = app.store_code(cw721_base_latest_contract()); - - let cw721 = app - .instantiate_contract( - code_id_016, - admin(), - &v16::InstantiateMsg { - name: "collection".to_string(), - symbol: "symbol".to_string(), - minter: admin().into_string(), - }, - &[], - "cw721-base", - Some(admin().into_string()), - ) - .unwrap(); - - mint_transfer_and_burn(&mut app, cw721.clone(), admin(), "1".to_string()); - - app.execute( - admin(), - WasmMsg::Migrate { - contract_addr: cw721.to_string(), - new_code_id: code_id_latest, - msg: to_json_binary(&Empty::default()).unwrap(), - } - .into(), - ) - .unwrap(); - - mint_transfer_and_burn(&mut app, cw721.clone(), admin(), "1".to_string()); - - // check new mint query response works. - let m: MinterResponse = app - .wrap() - .query_wasm_smart(&cw721, &crate::QueryMsg::::Minter {}) - .unwrap(); - assert_eq!(m.minter, Some(admin().to_string())); - - // check that the new response is backwards compatable when minter - // is not None. - let m: v16::MinterResponse = app - .wrap() - .query_wasm_smart(&cw721, &crate::QueryMsg::::Minter {}) - .unwrap(); - assert_eq!(m.minter, admin().to_string()); -} - -/// Test backward compatibility using instantiate msg from a 0.16 version on latest contract. -/// This ensures existing 3rd party contracts doesnt need to updated as well. -#[test] -fn test_instantiate_016_msg() { - use cw721_base_016 as v16; - let mut app = App::default(); - let admin = || Addr::unchecked("admin"); - - let code_id_latest = app.store_code(cw721_base_latest_contract()); - - let cw721 = app - .instantiate_contract( - code_id_latest, - admin(), - &v16::InstantiateMsg { - name: "collection".to_string(), - symbol: "symbol".to_string(), - minter: admin().into_string(), - }, - &[], - "cw721-base", - Some(admin().into_string()), - ) - .unwrap(); - - // assert withdraw address is None - let withdraw_addr: Option = app - .wrap() - .query_wasm_smart(cw721, &crate::QueryMsg::::GetWithdrawAddress {}) - .unwrap(); - assert!(withdraw_addr.is_none()); -} diff --git a/contracts/cw721-base/src/query.rs b/contracts/cw721-base/src/query.rs index 8ebc2f4b8..b0c897d75 100644 --- a/contracts/cw721-base/src/query.rs +++ b/contracts/cw721-base/src/query.rs @@ -1,372 +1,29 @@ +use cosmwasm_std::CustomMsg; +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::query::*; use serde::de::DeserializeOwned; use serde::Serialize; -use cosmwasm_std::{ - to_json_binary, Addr, Binary, BlockInfo, CustomMsg, Deps, Env, Order, StdError, StdResult, -}; - -use cw721::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721Query, - Expiration, MinterResponse, NftInfoResponse, NumTokensResponse, OperatorResponse, - OperatorsResponse, OwnerOfResponse, TokensResponse, -}; -use cw_storage_plus::Bound; -use cw_utils::maybe_addr; - -use crate::msg::QueryMsg; -use crate::state::{Approval, Cw721Contract, TokenInfo}; - -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 1000; - -impl<'a, T, C, E, Q> Cw721Query for Cw721Contract<'a, T, C, E, Q> +use crate::Cw721Contract; + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Query< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { - fn contract_info(&self, deps: Deps) -> StdResult { - self.contract_info.load(deps.storage) - } - - fn num_tokens(&self, deps: Deps) -> StdResult { - let count = self.token_count(deps.storage)?; - Ok(NumTokensResponse { count }) - } - - fn nft_info(&self, deps: Deps, token_id: String) -> StdResult> { - let info = self.tokens.load(deps.storage, &token_id)?; - Ok(NftInfoResponse { - token_uri: info.token_uri, - extension: info.extension, - }) - } - - fn owner_of( - &self, - deps: Deps, - env: Env, - token_id: String, - include_expired: bool, - ) -> 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 - 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 - 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(parse_approval) - .collect(); - Ok(OperatorsResponse { operators: res? }) - } - - 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 - 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 }) - } - - 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 }) - } - - 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? }) - } - - 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: humanize_approvals(&env.block, &info, include_expired), - }, - info: NftInfoResponse { - token_uri: info.token_uri, - extension: info.extension, - }, - }) - } -} - -impl<'a, T, C, E, Q> Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, -{ - pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Minter {} => to_json_binary(&self.minter(deps)?), - QueryMsg::ContractInfo {} => to_json_binary(&self.contract_info(deps)?), - QueryMsg::NftInfo { token_id } => to_json_binary(&self.nft_info(deps, token_id)?), - QueryMsg::OwnerOf { - token_id, - include_expired, - } => to_json_binary(&self.owner_of( - deps, - env, - token_id, - include_expired.unwrap_or(false), - )?), - QueryMsg::AllNftInfo { - token_id, - include_expired, - } => to_json_binary(&self.all_nft_info( - deps, - env, - token_id, - include_expired.unwrap_or(false), - )?), - QueryMsg::Operator { - owner, - operator, - include_expired, - } => to_json_binary(&self.operator( - deps, - env, - owner, - operator, - include_expired.unwrap_or(false), - )?), - QueryMsg::AllOperators { - owner, - include_expired, - start_after, - limit, - } => to_json_binary(&self.operators( - deps, - env, - owner, - include_expired.unwrap_or(false), - start_after, - limit, - )?), - QueryMsg::NumTokens {} => to_json_binary(&self.num_tokens(deps)?), - QueryMsg::Tokens { - owner, - start_after, - limit, - } => to_json_binary(&self.tokens(deps, owner, start_after, limit)?), - QueryMsg::AllTokens { start_after, limit } => { - to_json_binary(&self.all_tokens(deps, start_after, limit)?) - } - QueryMsg::Approval { - token_id, - spender, - include_expired, - } => to_json_binary(&self.approval( - deps, - env, - token_id, - spender, - include_expired.unwrap_or(false), - )?), - QueryMsg::Approvals { - token_id, - include_expired, - } => to_json_binary(&self.approvals( - deps, - env, - token_id, - include_expired.unwrap_or(false), - )?), - QueryMsg::Ownership {} => to_json_binary(&Self::ownership(deps)?), - QueryMsg::Extension { msg: _ } => Ok(Binary::default()), - QueryMsg::GetWithdrawAddress {} => { - to_json_binary(&self.withdraw_address.may_load(deps.storage)?) - } - } - } - - pub fn minter(&self, deps: Deps) -> StdResult { - let minter = cw_ownable::get_ownership(deps.storage)? - .owner - .map(|a| a.into_string()); - - Ok(MinterResponse { minter }) - } - - 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, - } } diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 48bca6bf5..b386e0615 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -1,152 +1,51 @@ -use schemars::JsonSchema; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use std::marker::PhantomData; - -use cosmwasm_std::{Addr, BlockInfo, CustomMsg, StdResult, Storage}; - -use cw721::{ContractInfoResponse, Cw721, Expiration}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cosmwasm_std::CustomMsg; -pub struct Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - Q: CustomMsg, - E: CustomMsg, -{ - 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, T>>, - pub withdraw_address: Item<'a, String>, +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::state::*; - pub(crate) _custom_response: PhantomData, - pub(crate) _custom_query: PhantomData, - pub(crate) _custom_execute: PhantomData, -} - -// This is a signal, the implementations are in other files -impl<'a, T, C, E, Q> Cw721 for Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, - E: CustomMsg, - Q: CustomMsg, +use serde::de::DeserializeOwned; +use serde::Serialize; + +pub struct Cw721Contract< + 'a, + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { -} - -impl Default for Cw721Contract<'static, T, C, E, Q> + pub config: Cw721Config< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >, +} + +impl Default + for Cw721Contract< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > where - T: Serialize + DeserializeOwned + Clone, - E: CustomMsg, - Q: CustomMsg, + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, { fn default() -> Self { - Self::new( - "nft_info", - "num_tokens", - "operators", - "tokens", - "tokens__owner", - "withdraw_address", - ) - } -} - -impl<'a, T, C, E, Q> Cw721Contract<'a, T, C, E, Q> -where - T: Serialize + DeserializeOwned + Clone, - E: CustomMsg, - Q: CustomMsg, -{ - fn new( - contract_key: &'a str, - token_count_key: &'a str, - operator_key: &'a str, - tokens_key: &'a str, - tokens_owner_key: &'a str, - withdraw_address_key: &'a str, - ) -> Self { - let indexes = TokenIndexes { - owner: MultiIndex::new(token_owner_idx, tokens_key, tokens_owner_key), - }; Self { - contract_info: Item::new(contract_key), - token_count: Item::new(token_count_key), - operators: Map::new(operator_key), - tokens: IndexedMap::new(tokens_key, indexes), - withdraw_address: Item::new(withdraw_address_key), - _custom_response: PhantomData, - _custom_execute: PhantomData, - _custom_query: PhantomData, + config: Cw721Config::default(), } } - - pub fn token_count(&self, storage: &dyn Storage) -> StdResult { - Ok(self.token_count.may_load(storage)?.unwrap_or_default()) - } - - pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { - let val = self.token_count(storage)? + 1; - self.token_count.save(storage, &val)?; - Ok(val) - } - - pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { - let val = self.token_count(storage)? - 1; - self.token_count.save(storage, &val)?; - Ok(val) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -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, - - /// You can add any custom metadata here when you extend cw721-base - pub extension: T, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -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) - } -} - -pub struct TokenIndexes<'a, T> -where - T: Serialize + DeserializeOwned + Clone, -{ - pub owner: MultiIndex<'a, Addr, TokenInfo, String>, -} - -impl<'a, T> IndexList> for TokenIndexes<'a, T> -where - T: Serialize + DeserializeOwned + Clone, -{ - fn get_indexes(&'_ self) -> Box>> + '_> { - let v: Vec<&dyn Index>> = vec![&self.owner]; - Box::new(v.into_iter()) - } -} - -pub fn token_owner_idx(_pk: &[u8], d: &TokenInfo) -> Addr { - d.owner.clone() } diff --git a/contracts/cw721-base/src/upgrades/mod.rs b/contracts/cw721-base/src/upgrades/mod.rs deleted file mode 100644 index bdb01c30d..000000000 --- a/contracts/cw721-base/src/upgrades/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod v0_17; diff --git a/contracts/cw721-base/src/upgrades/v0_17.rs b/contracts/cw721-base/src/upgrades/v0_17.rs deleted file mode 100644 index c0bd63b98..000000000 --- a/contracts/cw721-base/src/upgrades/v0_17.rs +++ /dev/null @@ -1,27 +0,0 @@ -use cosmwasm_std::{CustomMsg, DepsMut, Response}; -use cw721_base_016 as v16; -use serde::{de::DeserializeOwned, Serialize}; - -use crate::ContractError; - -pub fn migrate(deps: DepsMut) -> Result, ContractError> -where - T: Serialize + DeserializeOwned + Clone, - Q: CustomMsg, - E: CustomMsg, -{ - // remove old minter info - let tract16 = v16::Cw721Contract::::default(); - let minter = tract16.minter.load(deps.storage)?; - tract16.minter.remove(deps.storage); - - // save new ownership info - let ownership = cw_ownable::initialize_owner(deps.storage, deps.api, Some(minter.as_str()))?; - - Ok(Response::new() - .add_attribute("action", "migrate") - .add_attribute("from_version", "0.16.0") - .add_attribute("to_version", "0.17.0") - .add_attribute("old_minter", minter) - .add_attributes(ownership.into_attributes())) -} diff --git a/contracts/cw721-expiration/README.md b/contracts/cw721-expiration/README.md index d12c11c8c..43bf5c961 100644 --- a/contracts/cw721-expiration/README.md +++ b/contracts/cw721-expiration/README.md @@ -52,7 +52,7 @@ To generate an optimized build run: docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/rust-optimizer:0.14.0 + cosmwasm/rust-optimizer:0.16.0 ``` ### Testing diff --git a/contracts/cw721-expiration/examples/schema.rs b/contracts/cw721-expiration/examples/schema.rs index aadffcf75..560945fa3 100644 --- a/contracts/cw721-expiration/examples/schema.rs +++ b/contracts/cw721-expiration/examples/schema.rs @@ -1,10 +1,23 @@ -use cosmwasm_schema::write_api; -use cw721_expiration::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::{export_schema_with_title, remove_schemas, schema_for}; +use cosmwasm_std::Empty; +use cw721::state::DefaultOptionMetadataExtension; +use cw721_expiration::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use std::env::current_dir; +use std::fs::create_dir_all; fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + // entry points - generate always with title for avoiding name suffixes like "..._empty_for_..." due to generics + export_schema_with_title(&schema_for!(InstantiateMsg), &out_dir, "InstantiateMsg"); + export_schema_with_title( + &schema_for!(ExecuteMsg::), + &out_dir, + "ExecuteMsg", + ); + export_schema_with_title(&schema_for!(QueryMsg), &out_dir, "QueryMsg"); + export_schema_with_title(&schema_for!(MigrateMsg), &out_dir, "MigrateMsg"); } diff --git a/contracts/cw721-expiration/schema/cw721-expiration.json b/contracts/cw721-expiration/schema/cw721-expiration.json deleted file mode 100644 index 3da3c0405..000000000 --- a/contracts/cw721-expiration/schema/cw721-expiration.json +++ /dev/null @@ -1,1825 +0,0 @@ -{ - "contract_name": "cw721-expiration", - "contract_version": "0.18.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "type": "object", - "required": [ - "expiration_days", - "name", - "symbol" - ], - "properties": { - "expiration_days": { - "description": "max 65535 days", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "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", - "null" - ] - }, - "name": { - "description": "Name of the NFT contract", - "type": "string" - }, - "symbol": { - "description": "Symbol of the NFT contract", - "type": "string" - }, - "withdraw_address": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", - "oneOf": [ - { - "description": "Transfer is a base message to move a token to another account without triggering actions", - "type": "object", - "required": [ - "transfer_nft" - ], - "properties": { - "transfer_nft": { - "type": "object", - "required": [ - "recipient", - "token_id" - ], - "properties": { - "recipient": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted Approval", - "type": "object", - "required": [ - "revoke" - ], - "properties": { - "revoke": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted ApproveAll permission", - "type": "object", - "required": [ - "revoke_all" - ], - "properties": { - "revoke_all": { - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Mint a new NFT, can only be called by the contract minter", - "type": "object", - "required": [ - "mint" - ], - "properties": { - "mint": { - "type": "object", - "required": [ - "owner", - "token_id" - ], - "properties": { - "extension": { - "description": "Any custom extension used by this contract", - "anyOf": [ - { - "$ref": "#/definitions/Empty" - }, - { - "type": "null" - } - ] - }, - "owner": { - "description": "The owner of the newly minter NFT", - "type": "string" - }, - "token_id": { - "description": "Unique ID of the NFT", - "type": "string" - }, - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Burn an NFT the sender has access to", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension msg", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets address to send withdrawn fees to. Only owner can call this.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", - "type": "object", - "required": [ - "remove_withdraw_address" - ], - "properties": { - "remove_withdraw_address": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", - "type": "object", - "required": [ - "withdraw_funds" - ], - "properties": { - "withdraw_funds": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", - "type": "object", - "required": [ - "update_ownership" - ], - "properties": { - "update_ownership": { - "$ref": "#/definitions/Action" - } - }, - "additionalProperties": false - } - ], - "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" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "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 - } - ] - }, - "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" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "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", - "oneOf": [ - { - "description": "Return the owner of the given token, error if token does not exist", - "type": "object", - "required": [ - "owner_of" - ], - "properties": { - "owner_of": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return operator that can access all of the owner's tokens.", - "type": "object", - "required": [ - "approval" - ], - "properties": { - "approval": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approvals that a token has", - "type": "object", - "required": [ - "approvals" - ], - "properties": { - "approvals": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approval of a given operator for all tokens of an owner, error if not set", - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "object", - "required": [ - "operator", - "owner" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "operator": { - "type": "string" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "List all operators that can access all of the owner's tokens", - "type": "object", - "required": [ - "all_operators" - ], - "properties": { - "all_operators": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired items, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Total number of tokens issued, including all expired NFTs", - "type": "object", - "required": [ - "num_tokens" - ], - "properties": { - "num_tokens": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns top-level metadata about the contract", - "type": "object", - "required": [ - "contract_info" - ], - "properties": { - "contract_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", - "type": "object", - "required": [ - "nft_info" - ], - "properties": { - "nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", - "type": "object", - "required": [ - "all_nft_info" - ], - "properties": { - "all_nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", - "type": "object", - "required": [ - "tokens" - ], - "properties": { - "tokens": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", - "type": "object", - "required": [ - "all_tokens" - ], - "properties": { - "all_tokens": { - "type": "object", - "properties": { - "include_invalid": { - "description": "unset or false will filter out expired nfts, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return the minter", - "type": "object", - "required": [ - "minter" - ], - "properties": { - "minter": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension query", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query the contract's ownership information", - "type": "object", - "required": [ - "ownership" - ], - "properties": { - "ownership": { - "type": "object", - "additionalProperties": false - } - }, - "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" - } - } - }, - "migrate": null, - "sudo": null, - "responses": { - "all_nft_info": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllNftInfoResponse_for_Nullable_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_Nullable_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_Nullable_Empty": { - "type": "object", - "properties": { - "extension": { - "description": "You can add any custom metadata here when you extend cw721-base", - "anyOf": [ - { - "$ref": "#/definitions/Empty" - }, - { - "type": "null" - } - ] - }, - "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_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" - } - } - }, - "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 - }, - "extension": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Null", - "type": "null" - }, - "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_Nullable_Empty", - "type": "object", - "properties": { - "extension": { - "description": "You can add any custom metadata here when you extend cw721-base", - "anyOf": [ - { - "$ref": "#/definitions/Empty" - }, - { - "type": "null" - } - ] - }, - "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" - } - } - }, - "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_String", - "description": "The contract's ownership info", - "type": "object", - "properties": { - "owner": { - "description": "The contract's current owner. `None` if the ownership has been renounced.", - "type": [ - "string", - "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.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "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-expiration/schema/execute_msg.json b/contracts/cw721-expiration/schema/execute_msg.json new file mode 100644 index 000000000..2bcf2fea7 --- /dev/null +++ b/contracts/cw721-expiration/schema/execute_msg.json @@ -0,0 +1,562 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets address to send withdrawn fees to. Only owner can call this.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", + "type": "object", + "required": [ + "remove_withdraw_address" + ], + "properties": { + "remove_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", + "type": "object", + "required": [ + "withdraw_funds" + ], + "properties": { + "withdraw_funds": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "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" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "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 + } + ] + }, + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "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" + } + ] + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "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" + } + } +} diff --git a/contracts/cw721-expiration/schema/instantiate_msg.json b/contracts/cw721-expiration/schema/instantiate_msg.json new file mode 100644 index 000000000..f52482235 --- /dev/null +++ b/contracts/cw721-expiration/schema/instantiate_msg.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "expiration_days", + "name", + "symbol" + ], + "properties": { + "expiration_days": { + "description": "max 65535 days", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "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", + "null" + ] + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + }, + "withdraw_address": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/contracts/cw721-expiration/schema/migrate_msg.json b/contracts/cw721-expiration/schema/migrate_msg.json new file mode 100644 index 000000000..f83fa9508 --- /dev/null +++ b/contracts/cw721-expiration/schema/migrate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "with_update" + ], + "properties": { + "with_update": { + "type": "object", + "properties": { + "creator": { + "type": [ + "string", + "null" + ] + }, + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/cw721-expiration/schema/query_msg.json b/contracts/cw721-expiration/schema/query_msg.json new file mode 100644 index 000000000..8fd419769 --- /dev/null +++ b/contracts/cw721-expiration/schema/query_msg.json @@ -0,0 +1,458 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens.", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "include_expired_nft": { + "description": "unset or false will filter out expired nfts, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approval of a given operator for all tokens of an owner, error if not set", + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "operator", + "owner" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued, including all expired NFTs", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract", + "type": "object", + "required": [ + "get_collection_info" + ], + "properties": { + "get_collection_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_minter_ownership" + ], + "properties": { + "get_minter_ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return the minter", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension query", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_withdraw_address" + ], + "properties": { + "get_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "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" + } + } +} diff --git a/contracts/cw721-expiration/src/contract_tests.rs b/contracts/cw721-expiration/src/contract_tests.rs index 7cfe8f501..c17337ba2 100644 --- a/contracts/cw721-expiration/src/contract_tests.rs +++ b/contracts/cw721-expiration/src/contract_tests.rs @@ -3,33 +3,40 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ - from_json, to_json_binary, Addr, CosmosMsg, DepsMut, Response, StdError, WasmMsg, + from_json, to_json_binary, Addr, CosmosMsg, DepsMut, Empty, Response, StdError, WasmMsg, }; -use cw721::{ - Approval, ApprovalResponse, ContractInfoResponse, Cw721ReceiveMsg, Expiration, NftInfoResponse, - OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, +use cw721::error::Cw721ContractError; +use cw721::msg::{ + ApprovalResponse, Cw721ExecuteMsg, NftInfoResponse, OperatorResponse, OperatorsResponse, + OwnerOfResponse, TokensResponse, }; -use cw_ownable::OwnershipError; +use cw721::receiver::Cw721ReceiveMsg; +use cw721::state::{CollectionInfo, MINTER}; +use cw721::{query::Cw721Query, Approval, Expiration}; +use cw_ownable::{Action, Ownership, OwnershipError}; use crate::state::Cw721ExpirationContract; use crate::{ - error::ContractError, msg::ExecuteMsg, msg::InstantiateMsg, msg::QueryMsg, Extension, - MinterResponse, + error::ContractError, msg::InstantiateMsg, msg::QueryMsg, DefaultOptionMetadataExtension, }; -use cw721_base::ContractError as Cw721ContractError; -const MINTER: &str = "merlin"; +const MINTER_ADDR: &str = "minter"; +const CREATOR_ADDR: &str = "creator"; const CONTRACT_NAME: &str = "Magic Power"; const SYMBOL: &str = "MGK"; -fn setup_contract(deps: DepsMut<'_>, expiration_days: u16) -> Cw721ExpirationContract<'static> { - let contract = Cw721ExpirationContract::default(); +fn setup_contract( + deps: DepsMut<'_>, + expiration_days: u16, +) -> Cw721ExpirationContract<'static, DefaultOptionMetadataExtension, Empty, Empty, Empty> { + let contract = + Cw721ExpirationContract::::default(); let msg = InstantiateMsg { expiration_days, name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), - minter: Some(String::from(MINTER)), + minter: Some(String::from(MINTER_ADDR)), withdraw_address: None, }; let info = mock_info("creator", &[]); @@ -41,41 +48,115 @@ fn setup_contract(deps: DepsMut<'_>, expiration_days: u16) -> Cw721ExpirationCon #[test] fn proper_instantiation() { let mut deps = mock_dependencies(); - let contract = Cw721ExpirationContract::default(); + let contract = + Cw721ExpirationContract::::default(); let msg = InstantiateMsg { expiration_days: 1, name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), - minter: Some(String::from(MINTER)), - withdraw_address: None, + minter: Some(String::from(MINTER_ADDR)), + withdraw_address: Some(String::from(CREATOR_ADDR)), }; let info = mock_info("creator", &[]); + let env = mock_env(); // we can just call .unwrap() to assert this was a success let res = contract - .instantiate(deps.as_mut(), mock_env(), info, msg) + .instantiate(deps.as_mut(), env.clone(), info, msg) .unwrap(); assert_eq!(0, res.messages.len()); // it worked, let's query the state - let res = contract.minter(deps.as_ref()).unwrap(); - assert_eq!(Some(MINTER.to_string()), res.minter); - let info = contract.contract_info(deps.as_ref()).unwrap(); + let minter_ownership = MINTER.get_ownership(deps.as_ref().storage).unwrap(); + assert_eq!(Some(Addr::unchecked(MINTER_ADDR)), minter_ownership.owner); + let collection_info = contract + .base_contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!( - info, - ContractInfoResponse { + collection_info, + CollectionInfo { name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), } ); - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let withdraw_address = contract + .base_contract + .config + .withdraw_address + .may_load(deps.as_ref().storage) + .unwrap(); + assert_eq!(Some(CREATOR_ADDR.to_string()), withdraw_address); + + let count = contract + .base_contract + .query_num_tokens(deps.as_ref(), env) + .unwrap(); assert_eq!(0, count.count); // list the token_ids let tokens = contract - .all_tokens(deps.as_ref(), mock_env(), None, None, false) + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, None, false) + .unwrap(); + assert_eq!(0, tokens.tokens.len()); +} + +#[test] +fn proper_instantiation_with_collection_info() { + let mut deps = mock_dependencies(); + let contract = + Cw721ExpirationContract::::default(); + + let msg = InstantiateMsg { + expiration_days: 1, + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + minter: Some(String::from(MINTER_ADDR)), + withdraw_address: Some(String::from(CREATOR_ADDR)), + }; + let info = mock_info("creator", &[]); + let env = mock_env(); + + // we can just call .unwrap() to assert this was a success + let res = contract + .instantiate(deps.as_mut(), env.clone(), info, msg) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let minter_ownership = MINTER.get_ownership(deps.as_ref().storage).unwrap(); + assert_eq!(Some(Addr::unchecked(MINTER_ADDR)), minter_ownership.owner); + let collection_info = contract + .base_contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap(); + assert_eq!( + collection_info, + CollectionInfo { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + } + ); + + let withdraw_address = contract + .base_contract + .config + .withdraw_address + .may_load(deps.as_ref().storage) + .unwrap(); + assert_eq!(Some(CREATOR_ADDR.to_string()), withdraw_address); + + let count = contract + .base_contract + .query_num_tokens(deps.as_ref(), env) + .unwrap(); + assert_eq!(0, count.count); + + // list the token_ids + let tokens = contract + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, None, false) .unwrap(); assert_eq!(0, tokens.tokens.len()); } @@ -88,7 +169,7 @@ fn test_mint() { let token_id = "atomize".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/atomize".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("medusa"), token_uri: Some(token_uri.clone()), @@ -97,8 +178,9 @@ fn test_mint() { // random cannot mint let random = mock_info("random", &[]); + let env = mock_env(); let err = contract - .execute(deps.as_mut(), mock_env(), random, mint_msg.clone()) + .execute(deps.as_mut(), env.clone(), random, mint_msg.clone()) .unwrap_err(); assert_eq!( err, @@ -106,27 +188,30 @@ fn test_mint() { ); // minter can mint - let allowed = mock_info(MINTER, &[]); + let allowed = mock_info(MINTER_ADDR, &[]); let _ = contract .execute(deps.as_mut(), mock_env(), allowed, mint_msg) .unwrap(); // ensure num tokens increases - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let count = contract + .base_contract + .query_num_tokens(deps.as_ref(), env) + .unwrap(); assert_eq!(1, count.count); // unknown nft returns error let _ = contract - .nft_info(deps.as_ref(), mock_env(), "unknown".to_string(), false) + .query_nft_info_include_expired_nft(deps.as_ref(), mock_env(), "unknown".to_string(), false) .unwrap_err(); // this nft info is correct let info = contract - .nft_info(deps.as_ref(), mock_env(), token_id.clone(), false) + .query_nft_info_include_expired_nft(deps.as_ref(), mock_env(), token_id.clone(), false) .unwrap(); assert_eq!( info, - NftInfoResponse:: { + NftInfoResponse:: { token_uri: Some(token_uri), extension: None, } @@ -134,7 +219,13 @@ fn test_mint() { // owner info is correct let owner = contract - .owner_of(deps.as_ref(), mock_env(), token_id.clone(), true, false) + .query_owner_of_include_expired_nft( + deps.as_ref(), + mock_env(), + token_id.clone(), + true, + false, + ) .unwrap(); assert_eq!( owner, @@ -152,14 +243,14 @@ fn test_mint() { assert_eq!(mint_timestamp, mock_env().block.time); // Cannot mint same token_id again - let mint_msg2 = ExecuteMsg::Mint { + let mint_msg2 = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("hercules"), token_uri: None, extension: None, }; - let allowed = mock_info(MINTER, &[]); + let allowed = mock_info(MINTER_ADDR, &[]); let err = contract .execute(deps.as_mut(), mock_env(), allowed, mint_msg2) .unwrap_err(); @@ -167,7 +258,7 @@ fn test_mint() { // list the token_ids let tokens = contract - .all_tokens(deps.as_ref(), mock_env(), None, None, false) + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, None, false) .unwrap(); assert_eq!(1, tokens.tokens.len()); assert_eq!(vec![token_id], tokens.tokens); @@ -181,7 +272,7 @@ fn test_update_minter() { let token_id = "petrify".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id, owner: String::from("medusa"), token_uri: Some(token_uri.clone()), @@ -189,7 +280,7 @@ fn test_update_minter() { }; // Minter can mint - let minter_info = mock_info(MINTER, &[]); + let minter_info = mock_info(MINTER_ADDR, &[]); let _ = contract .execute(deps.as_mut(), mock_env(), minter_info.clone(), mint_msg) .unwrap(); @@ -201,7 +292,7 @@ fn test_update_minter() { deps.as_mut(), mock_env(), minter_info.clone(), - ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + Cw721ExecuteMsg::UpdateOwnership(Action::TransferOwnership { new_owner: "random".to_string(), expiry: None, }), @@ -209,26 +300,18 @@ fn test_update_minter() { .unwrap(); // Minter does not change until ownership transfer completes. - let minter: MinterResponse = from_json( - contract - .query(deps.as_ref(), mock_env(), QueryMsg::Minter {}) - .unwrap(), - ) - .unwrap(); - assert_eq!(minter.minter, Some(MINTER.to_string())); - // Pending ownership transfer should be discoverable via query. - let ownership: cw_ownable::Ownership = from_json( + let ownership: Ownership = from_json( contract - .query(deps.as_ref(), mock_env(), QueryMsg::Ownership {}) + .query(deps.as_ref(), mock_env(), QueryMsg::GetMinterOwnership {}) .unwrap(), ) .unwrap(); assert_eq!( ownership, - cw_ownable::Ownership:: { - owner: Some(Addr::unchecked(MINTER)), + Ownership:: { + owner: Some(Addr::unchecked(MINTER_ADDR)), pending_owner: Some(Addr::unchecked("random")), pending_expiry: None, } @@ -241,20 +324,20 @@ fn test_update_minter() { deps.as_mut(), mock_env(), random_info.clone(), - ExecuteMsg::UpdateOwnership(cw_ownable::Action::AcceptOwnership), + Cw721ExecuteMsg::UpdateOwnership(Action::AcceptOwnership), ) .unwrap(); // Minter changes after ownership transfer is accepted. - let minter: MinterResponse = from_json( + let minter_ownership: Ownership = from_json( contract - .query(deps.as_ref(), mock_env(), QueryMsg::Minter {}) + .query(deps.as_ref(), mock_env(), QueryMsg::GetMinterOwnership {}) .unwrap(), ) .unwrap(); - assert_eq!(minter.minter, Some("random".to_string())); + assert_eq!(minter_ownership.owner, Some(random_info.sender.clone())); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: "randoms_token".to_string(), owner: String::from("medusa"), token_uri: Some(token_uri), @@ -284,20 +367,20 @@ fn test_burn() { let token_id = "petrify".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), - owner: MINTER.to_string(), + owner: MINTER_ADDR.to_string(), token_uri: Some(token_uri), extension: None, }; - let burn_msg = ExecuteMsg::Burn { + let burn_msg = Cw721ExecuteMsg::Burn { token_id: token_id.clone(), }; // mint some NFT let mut env = mock_env(); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let _ = contract .execute(deps.as_mut(), env.clone(), minter.clone(), mint_msg.clone()) .unwrap(); @@ -318,17 +401,25 @@ fn test_burn() { .unwrap(); // ensure num tokens decreases - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let count = contract + .base_contract + .query_num_tokens(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!(0, count.count); // trying to get nft returns error let _ = contract - .nft_info(deps.as_ref(), env.clone(), "petrify".to_string(), false) + .query_nft_info_include_expired_nft( + deps.as_ref(), + env.clone(), + "petrify".to_string(), + false, + ) .unwrap_err(); // list the token_ids let tokens = contract - .all_tokens(deps.as_ref(), env.clone(), None, None, false) + .query_all_tokens_include_expired_nft(deps.as_ref(), env.clone(), None, None, false) .unwrap(); assert!(tokens.tokens.is_empty()); @@ -364,7 +455,7 @@ fn test_transfer_nft() { let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); let owner = "owner"; - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from(owner), token_uri: Some(token_uri), @@ -372,14 +463,14 @@ fn test_transfer_nft() { }; let mut env = mock_env(); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), env.clone(), minter, mint_msg) .unwrap(); // random cannot transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("random"), token_id: token_id.clone(), }; @@ -395,7 +486,7 @@ fn test_transfer_nft() { // owner can let owner_info = mock_info(owner, &[]); let new_owner = "random"; - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from(new_owner), token_id: token_id.clone(), }; @@ -444,7 +535,7 @@ fn test_send_nft() { let token_id = "melt".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("venus"), token_uri: Some(token_uri), @@ -452,14 +543,14 @@ fn test_send_nft() { }; let mut env = mock_env(); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), env.clone(), minter, mint_msg) .unwrap(); let msg = to_json_binary("You now have the melting power").unwrap(); let target = String::from("another_contract"); - let send_msg = ExecuteMsg::SendNft { + let send_msg = Cw721ExecuteMsg::SendNft { contract: target.clone(), token_id: token_id.clone(), msg: msg.clone(), @@ -530,7 +621,7 @@ fn test_approve_revoke() { let token_id = "grow".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/grow".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("demeter"), token_uri: Some(token_uri), @@ -538,14 +629,14 @@ fn test_approve_revoke() { }; let mut env = mock_env(); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), env.clone(), minter, mint_msg) .unwrap(); // token owner shows in approval query let res = contract - .approval( + .query_approval_include_expired_nft( deps.as_ref(), env.clone(), token_id.clone(), @@ -558,14 +649,14 @@ fn test_approve_revoke() { res, ApprovalResponse { approval: Approval { - spender: String::from("demeter"), + spender: Addr::unchecked("demeter"), expires: Expiration::Never {} } } ); // Give random transferring power - let approve_msg = ExecuteMsg::Approve { + let approve_msg = Cw721ExecuteMsg::Approve { spender: String::from("random"), token_id: token_id.clone(), expires: None, @@ -585,7 +676,7 @@ fn test_approve_revoke() { // test approval query let res = contract - .approval( + .query_approval_include_expired_nft( deps.as_ref(), env.clone(), token_id.clone(), @@ -598,7 +689,7 @@ fn test_approve_revoke() { res, ApprovalResponse { approval: Approval { - spender: String::from("random"), + spender: Addr::unchecked("random"), expires: Expiration::Never {} } } @@ -606,7 +697,7 @@ fn test_approve_revoke() { // random can now transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("person"), token_id: token_id.clone(), }; @@ -618,7 +709,7 @@ fn test_approve_revoke() { let query_msg = QueryMsg::OwnerOf { token_id: token_id.clone(), include_expired: None, - include_invalid: None, + include_expired_nft: None, }; let res: OwnerOfResponse = from_json( contract @@ -635,7 +726,7 @@ fn test_approve_revoke() { ); // Approve, revoke, and check for empty, to test revoke - let approve_msg = ExecuteMsg::Approve { + let approve_msg = Cw721ExecuteMsg::Approve { spender: String::from("random"), token_id: token_id.clone(), expires: None, @@ -650,7 +741,7 @@ fn test_approve_revoke() { ) .unwrap(); - let revoke_msg = ExecuteMsg::Revoke { + let revoke_msg = Cw721ExecuteMsg::Revoke { spender: String::from("random"), token_id: token_id.clone(), }; @@ -720,19 +811,19 @@ fn test_approve_all_revoke_all() { let token_id2 = "grow2".to_string(); let token_uri2 = "https://www.merriam-webster.com/dictionary/grow2".to_string(); - let mint_msg1 = ExecuteMsg::Mint { + let mint_msg1 = Cw721ExecuteMsg::Mint { token_id: token_id1.clone(), owner: String::from("demeter"), token_uri: Some(token_uri1), extension: None, }; - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg1) .unwrap(); - let mint_msg2 = ExecuteMsg::Mint { + let mint_msg2 = Cw721ExecuteMsg::Mint { token_id: token_id2.clone(), owner: String::from("demeter"), token_uri: Some(token_uri2), @@ -745,12 +836,12 @@ fn test_approve_all_revoke_all() { // paginate the token_ids let tokens = contract - .all_tokens(deps.as_ref(), mock_env(), None, Some(1), false) + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, Some(1), false) .unwrap(); assert_eq!(1, tokens.tokens.len()); assert_eq!(vec![token_id1.clone()], tokens.tokens); let tokens = contract - .all_tokens( + .query_all_tokens_include_expired_nft( deps.as_ref(), mock_env(), Some(token_id1.clone()), @@ -762,7 +853,7 @@ fn test_approve_all_revoke_all() { assert_eq!(vec![token_id2.clone()], tokens.tokens); // demeter gives random full (operator) power over her tokens - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("random"), expires: None, }; @@ -780,7 +871,7 @@ fn test_approve_all_revoke_all() { // random can now transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("person"), token_id: token_id1, }; @@ -796,7 +887,7 @@ fn test_approve_all_revoke_all() { }; let msg: CosmosMsg = CosmosMsg::Wasm(inner_msg); - let send_msg = ExecuteMsg::SendNft { + let send_msg = Cw721ExecuteMsg::SendNft { contract: String::from("another_contract"), token_id: token_id2, msg: to_json_binary(&msg).unwrap(), @@ -806,7 +897,7 @@ fn test_approve_all_revoke_all() { .unwrap(); // Approve_all, revoke_all, and check for empty, to test revoke_all - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("operator"), expires: None, }; @@ -818,7 +909,8 @@ fn test_approve_all_revoke_all() { // query for operator should return approval let res = contract - .operator( + .base_contract + .query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -830,14 +922,14 @@ fn test_approve_all_revoke_all() { res, OperatorResponse { approval: Approval { - spender: String::from("operator"), + spender: Addr::unchecked("operator"), expires: Expiration::Never {} } } ); // query for other should throw error - let res = contract.operator( + let res = contract.base_contract.query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -850,7 +942,8 @@ fn test_approve_all_revoke_all() { } let res = contract - .operators( + .base_contract + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -863,7 +956,7 @@ fn test_approve_all_revoke_all() { res, OperatorsResponse { operators: vec![cw721::Approval { - spender: String::from("operator"), + spender: Addr::unchecked("operator"), expires: Expiration::Never {} }] } @@ -871,7 +964,7 @@ fn test_approve_all_revoke_all() { // second approval let buddy_expires = Expiration::AtHeight(1234567); - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("buddy"), expires: Some(buddy_expires), }; @@ -882,7 +975,8 @@ fn test_approve_all_revoke_all() { // and paginate queries let res = contract - .operators( + .base_contract + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -895,13 +989,14 @@ fn test_approve_all_revoke_all() { res, OperatorsResponse { operators: vec![cw721::Approval { - spender: String::from("buddy"), + spender: Addr::unchecked("buddy"), expires: buddy_expires, }] } ); let res = contract - .operators( + .base_contract + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -914,13 +1009,13 @@ fn test_approve_all_revoke_all() { res, OperatorsResponse { operators: vec![cw721::Approval { - spender: String::from("operator"), + spender: Addr::unchecked("operator"), expires: Expiration::Never {} }] } ); - let revoke_all_msg = ExecuteMsg::RevokeAll { + let revoke_all_msg = Cw721ExecuteMsg::RevokeAll { operator: String::from("operator"), }; contract @@ -928,7 +1023,7 @@ fn test_approve_all_revoke_all() { .unwrap(); // query for operator should return error - let res = contract.operator( + let res = contract.base_contract.query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -942,7 +1037,8 @@ fn test_approve_all_revoke_all() { // Approvals are removed / cleared without affecting others let res = contract - .operators( + .base_contract + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -955,7 +1051,7 @@ fn test_approve_all_revoke_all() { res, OperatorsResponse { operators: vec![cw721::Approval { - spender: String::from("buddy"), + spender: Addr::unchecked("buddy"), expires: buddy_expires, }] } @@ -965,7 +1061,8 @@ fn test_approve_all_revoke_all() { let mut late_env = mock_env(); late_env.block.height = 1234568; //expired let res = contract - .operators( + .base_contract + .query_operators( deps.as_ref(), late_env.clone(), String::from("person"), @@ -977,7 +1074,7 @@ fn test_approve_all_revoke_all() { assert_eq!(0, res.operators.len()); // query operator should also return error - let res = contract.operator( + let res = contract.base_contract.query_operator( deps.as_ref(), late_env, String::from("person"), @@ -995,7 +1092,7 @@ fn test_approve_all_revoke_all() { fn test_tokens_by_owner() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); // Mint a couple tokens (from the same owner) let token_id1 = "grow1".to_string(); @@ -1004,7 +1101,7 @@ fn test_tokens_by_owner() { let ceres = String::from("ceres"); let token_id3 = "sing".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id1.clone(), owner: demeter.clone(), token_uri: None, @@ -1014,7 +1111,7 @@ fn test_tokens_by_owner() { .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) .unwrap(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id2.clone(), owner: ceres.clone(), token_uri: None, @@ -1024,7 +1121,7 @@ fn test_tokens_by_owner() { .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) .unwrap(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id3.clone(), owner: demeter.clone(), token_uri: None, @@ -1037,16 +1134,16 @@ fn test_tokens_by_owner() { // get all tokens in order: let expected = vec![token_id1.clone(), token_id2.clone(), token_id3.clone()]; let tokens = contract - .all_tokens(deps.as_ref(), mock_env(), None, None, false) + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, None, false) .unwrap(); assert_eq!(&expected, &tokens.tokens); // paginate let tokens = contract - .all_tokens(deps.as_ref(), mock_env(), None, Some(2), false) + .query_all_tokens_include_expired_nft(deps.as_ref(), mock_env(), None, Some(2), false) .unwrap(); assert_eq!(&expected[..2], &tokens.tokens[..]); let tokens = contract - .all_tokens( + .query_all_tokens_include_expired_nft( deps.as_ref(), mock_env(), Some(expected[1].clone()), @@ -1061,7 +1158,7 @@ fn test_tokens_by_owner() { let by_demeter = vec![token_id1, token_id3]; // all tokens by owner let tokens = contract - .tokens( + .query_tokens_include_expired_nft( deps.as_ref(), mock_env(), demeter.clone(), @@ -1072,13 +1169,13 @@ fn test_tokens_by_owner() { .unwrap(); assert_eq!(&by_demeter, &tokens.tokens); let tokens = contract - .tokens(deps.as_ref(), mock_env(), ceres, None, None, false) + .query_tokens_include_expired_nft(deps.as_ref(), mock_env(), ceres, None, None, false) .unwrap(); assert_eq!(&by_ceres, &tokens.tokens); // paginate for demeter let tokens = contract - .tokens( + .query_tokens_include_expired_nft( deps.as_ref(), mock_env(), demeter.clone(), @@ -1089,7 +1186,7 @@ fn test_tokens_by_owner() { .unwrap(); assert_eq!(&by_demeter[..1], &tokens.tokens[..]); let tokens = contract - .tokens( + .query_tokens_include_expired_nft( deps.as_ref(), mock_env(), demeter, @@ -1105,13 +1202,13 @@ fn test_tokens_by_owner() { fn test_nft_info() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner, token_uri: None, @@ -1123,7 +1220,7 @@ fn test_nft_info() { // assert valid nft is returned contract - .nft_info(deps.as_ref(), env.clone(), token_id.clone(), false) + .query_nft_info_include_expired_nft(deps.as_ref(), env.clone(), token_id.clone(), false) .unwrap(); // assert invalid nft throws error @@ -1131,7 +1228,7 @@ fn test_nft_info() { let expiration = env.block.time.plus_days(1); env.block.time = expiration; let error = contract - .nft_info(deps.as_ref(), env, token_id.clone(), false) + .query_nft_info_include_expired_nft(deps.as_ref(), env, token_id.clone(), false) .unwrap_err(); assert_eq!( error, @@ -1147,13 +1244,13 @@ fn test_nft_info() { fn test_all_nft_info() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner, token_uri: None, @@ -1165,7 +1262,13 @@ fn test_all_nft_info() { // assert valid nft is returned contract - .all_nft_info(deps.as_ref(), env.clone(), token_id.clone(), false, false) + .query_all_nft_info_include_expired_nft( + deps.as_ref(), + env.clone(), + token_id.clone(), + false, + false, + ) .unwrap(); // assert invalid nft throws error @@ -1173,7 +1276,7 @@ fn test_all_nft_info() { let expiration = env.block.time.plus_days(1); env.block.time = expiration; let error = contract - .all_nft_info(deps.as_ref(), env, token_id.clone(), false, false) + .query_all_nft_info_include_expired_nft(deps.as_ref(), env, token_id.clone(), false, false) .unwrap_err(); assert_eq!( error, @@ -1189,13 +1292,13 @@ fn test_all_nft_info() { fn test_owner_of() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner, token_uri: None, @@ -1207,7 +1310,13 @@ fn test_owner_of() { // assert valid nft is returned contract - .owner_of(deps.as_ref(), env.clone(), token_id.clone(), false, false) + .query_owner_of_include_expired_nft( + deps.as_ref(), + env.clone(), + token_id.clone(), + false, + false, + ) .unwrap(); // assert invalid nft throws error @@ -1215,7 +1324,7 @@ fn test_owner_of() { let expiration = env.block.time.plus_days(1); env.block.time = expiration; let error = contract - .owner_of(deps.as_ref(), env, token_id.clone(), false, false) + .query_owner_of_include_expired_nft(deps.as_ref(), env, token_id.clone(), false, false) .unwrap_err(); assert_eq!( error, @@ -1231,13 +1340,13 @@ fn test_owner_of() { fn test_approval() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: owner.clone(), token_uri: None, @@ -1249,7 +1358,7 @@ fn test_approval() { // assert valid nft is returned contract - .approval( + .query_approval_include_expired_nft( deps.as_ref(), env.clone(), token_id.clone(), @@ -1264,7 +1373,14 @@ fn test_approval() { let expiration = env.block.time.plus_days(1); env.block.time = expiration; let error = contract - .approval(deps.as_ref(), env, token_id.clone(), owner, false, false) + .query_approval_include_expired_nft( + deps.as_ref(), + env, + token_id.clone(), + owner, + false, + false, + ) .unwrap_err(); assert_eq!( error, @@ -1280,13 +1396,13 @@ fn test_approval() { fn test_approvals() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner, token_uri: None, @@ -1298,7 +1414,13 @@ fn test_approvals() { // assert valid nft is returned contract - .approvals(deps.as_ref(), env.clone(), token_id.clone(), false, false) + .query_approvals_include_expired_nft( + deps.as_ref(), + env.clone(), + token_id.clone(), + false, + false, + ) .unwrap(); // assert invalid nft throws error @@ -1306,7 +1428,7 @@ fn test_approvals() { let expiration = env.block.time.plus_days(1); env.block.time = expiration; let error = contract - .approvals(deps.as_ref(), env, token_id.clone(), false, false) + .query_approvals_include_expired_nft(deps.as_ref(), env, token_id.clone(), false, false) .unwrap_err(); assert_eq!( error, @@ -1322,13 +1444,13 @@ fn test_approvals() { fn test_tokens() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: owner.clone(), token_uri: None, @@ -1340,20 +1462,34 @@ fn test_tokens() { // assert valid nft is returned contract - .tokens(deps.as_ref(), env.clone(), owner.clone(), None, None, false) + .query_tokens_include_expired_nft( + deps.as_ref(), + env.clone(), + owner.clone(), + None, + None, + false, + ) .unwrap(); // assert invalid nft is not returned let expiration = env.block.time.plus_days(1); env.block.time = expiration; let tokens = contract - .tokens(deps.as_ref(), env.clone(), owner.clone(), None, None, false) + .query_tokens_include_expired_nft( + deps.as_ref(), + env.clone(), + owner.clone(), + None, + None, + false, + ) .unwrap(); assert_eq!(tokens, TokensResponse { tokens: vec![] }); // assert invalid nft is returned let tokens = contract - .tokens(deps.as_ref(), env, owner, None, None, true) + .query_tokens_include_expired_nft(deps.as_ref(), env, owner, None, None, true) .unwrap(); assert_eq!( tokens, @@ -1367,13 +1503,13 @@ fn test_tokens() { fn test_all_tokens() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut(), 1); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); let token_id = "grow1".to_string(); let owner = String::from("ark"); let mut env = mock_env(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: owner.clone(), token_uri: None, @@ -1385,20 +1521,20 @@ fn test_all_tokens() { // assert valid nft is returned contract - .all_tokens(deps.as_ref(), env.clone(), None, None, false) + .query_all_tokens_include_expired_nft(deps.as_ref(), env.clone(), None, None, false) .unwrap(); // assert invalid nft is not returned let expiration = env.block.time.plus_days(1); env.block.time = expiration; let tokens = contract - .tokens(deps.as_ref(), env.clone(), owner, None, None, false) + .query_tokens_include_expired_nft(deps.as_ref(), env.clone(), owner, None, None, false) .unwrap(); assert_eq!(tokens, TokensResponse { tokens: vec![] }); // assert invalid nft is returned let tokens = contract - .all_tokens(deps.as_ref(), env, None, None, true) + .query_all_tokens_include_expired_nft(deps.as_ref(), env, None, None, true) .unwrap(); assert_eq!( tokens, diff --git a/contracts/cw721-expiration/src/error.rs b/contracts/cw721-expiration/src/error.rs index bf041df59..211c6ceef 100644 --- a/contracts/cw721-expiration/src/error.rs +++ b/contracts/cw721-expiration/src/error.rs @@ -1,5 +1,5 @@ use cosmwasm_std::Timestamp; -use cw721_base::error::ContractError as Cw721ContractError; +use cw721::error::Cw721ContractError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] diff --git a/contracts/cw721-expiration/src/execute.rs b/contracts/cw721-expiration/src/execute.rs index d698bf3e1..7755f0dd7 100644 --- a/contracts/cw721-expiration/src/execute.rs +++ b/contracts/cw721-expiration/src/execute.rs @@ -1,29 +1,52 @@ -use cosmwasm_std::{ - Addr, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, Storage, +use cosmwasm_std::{Binary, CustomMsg, DepsMut, Env, MessageInfo, Response}; +use cw721::{ + execute::Cw721Execute, + msg::{Cw721ExecuteMsg, Cw721InstantiateMsg}, + Expiration, }; -use cw721::{Cw721Execute, Expiration}; -use cw721_base::Cw721Contract; +use serde::de::DeserializeOwned; +use serde::Serialize; use crate::{ - error::ContractError, msg::ExecuteMsg, msg::InstantiateMsg, state::Cw721ExpirationContract, - Extension, + error::ContractError, msg::InstantiateMsg, state::Cw721ExpirationContract, CONTRACT_NAME, + CONTRACT_VERSION, }; -use cw721_base::InstantiateMsg as Cw721InstantiateMsg; -impl<'a> Cw721ExpirationContract<'a> { +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721ExpirationContract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + // -- instantiate -- pub fn instantiate( &self, deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg, - ) -> Result { + ) -> Result, ContractError> { if msg.expiration_days == 0 { return Err(ContractError::MinExpiration {}); } - self.expiration_days + let contract = Cw721ExpirationContract::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + contract + .expiration_days .save(deps.storage, &msg.expiration_days)?; - Ok(self.base_contract.instantiate( + Ok(contract.base_contract.instantiate( deps, env, info, @@ -33,61 +56,63 @@ impl<'a> Cw721ExpirationContract<'a> { minter: msg.minter, withdraw_address: msg.withdraw_address, }, + CONTRACT_NAME, + CONTRACT_VERSION, )?) } + // -- execute -- pub fn execute( &self, deps: DepsMut, env: Env, info: MessageInfo, - msg: ExecuteMsg, - ) -> Result { + msg: Cw721ExecuteMsg, + ) -> Result, ContractError> { + let contract = Cw721ExpirationContract::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); match msg { - ExecuteMsg::Mint { + Cw721ExecuteMsg::Mint { token_id, owner, token_uri, extension, - } => self.mint(deps, env, info, token_id, owner, token_uri, extension), - ExecuteMsg::Approve { + } => { + contract.mint_with_timestamp(deps, env, info, token_id, owner, token_uri, extension) + } + Cw721ExecuteMsg::Approve { spender, token_id, expires, - } => self.approve(deps, env, info, spender, token_id, expires), - ExecuteMsg::Revoke { spender, token_id } => { - self.revoke(deps, env, info, spender, token_id) - } - ExecuteMsg::ApproveAll { operator, expires } => { - self.approve_all(deps, env, info, operator, expires) + } => contract.approve_include_nft_expired(deps, env, info, spender, token_id, expires), + Cw721ExecuteMsg::Revoke { spender, token_id } => { + contract.revoke_include_nft_expired(deps, env, info, spender, token_id) } - ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator), - ExecuteMsg::TransferNft { + Cw721ExecuteMsg::TransferNft { recipient, token_id, - } => self.transfer_nft(deps, env, info, recipient, token_id), - ExecuteMsg::SendNft { - contract, + } => contract.transfer_nft_include_nft_expired(deps, env, info, recipient, token_id), + Cw721ExecuteMsg::SendNft { + contract: recipient, token_id, msg, - } => self.send_nft(deps, env, info, contract, token_id, msg), - ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id), - ExecuteMsg::UpdateOwnership(action) => Self::update_ownership(deps, env, info, action), - ExecuteMsg::Extension { msg: _ } => Ok(Response::default()), - ExecuteMsg::SetWithdrawAddress { address } => { - self.set_withdraw_address(deps, &info.sender, address) + } => contract.send_nft_include_nft_expired(deps, env, info, recipient, token_id, msg), + Cw721ExecuteMsg::Burn { token_id } => { + contract.burn_nft_include_nft_expired(deps, env, info, token_id) } - ExecuteMsg::RemoveWithdrawAddress {} => { - self.remove_withdraw_address(deps.storage, &info.sender) + _ => { + let response = contract.base_contract.execute(deps, env, info, msg)?; + Ok(response) } - ExecuteMsg::WithdrawFunds { amount } => self.withdraw_funds(deps.storage, &amount), } } -} -impl<'a> Cw721ExpirationContract<'a> { #[allow(clippy::too_many_arguments)] - pub fn mint( + pub fn mint_with_timestamp( &self, deps: DepsMut, env: Env, @@ -95,8 +120,8 @@ impl<'a> Cw721ExpirationContract<'a> { token_id: String, owner: String, token_uri: Option, - extension: Extension, - ) -> Result { + extension: TMetadataExtension, + ) -> Result, ContractError> { let mint_timstamp = env.block.time; self.mint_timestamps .save(deps.storage, &token_id, &mint_timstamp)?; @@ -107,81 +132,7 @@ impl<'a> Cw721ExpirationContract<'a> { Ok(res) } - pub fn update_ownership( - deps: DepsMut, - env: Env, - info: MessageInfo, - action: cw_ownable::Action, - ) -> Result { - Ok( - Cw721Contract::::update_ownership( - deps, env, info, action, - )?, - ) - } - - pub fn set_withdraw_address( - &self, - deps: DepsMut, - sender: &Addr, - address: String, - ) -> Result { - Ok(self - .base_contract - .set_withdraw_address(deps, sender, address)?) - } - - pub fn remove_withdraw_address( - &self, - storage: &mut dyn Storage, - sender: &Addr, - ) -> Result { - Ok(self - .base_contract - .remove_withdraw_address(storage, sender)?) - } - - pub fn withdraw_funds( - &self, - storage: &mut dyn Storage, - amount: &Coin, - ) -> Result { - Ok(self.base_contract.withdraw_funds(storage, amount)?) - } -} - -// execute -impl<'a> Cw721ExpirationContract<'a> { - fn transfer_nft( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - recipient: String, - token_id: String, - ) -> Result, ContractError> { - self.assert_valid_nft(deps.as_ref(), &env, &token_id)?; - Ok(self - .base_contract - .transfer_nft(deps, env, info, recipient, token_id)?) - } - - fn send_nft( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - contract: String, - token_id: String, - msg: Binary, - ) -> Result, ContractError> { - self.assert_valid_nft(deps.as_ref(), &env, &token_id)?; - Ok(self - .base_contract - .send_nft(deps, env, info, contract, token_id, msg)?) - } - - fn approve( + pub fn approve_include_nft_expired( &self, deps: DepsMut, env: Env, @@ -189,94 +140,64 @@ impl<'a> Cw721ExpirationContract<'a> { spender: String, token_id: String, expires: Option, - ) -> Result, ContractError> { - self.assert_valid_nft(deps.as_ref(), &env, &token_id)?; + ) -> Result, ContractError> { + self.assert_nft_expired(deps.as_ref(), &env, token_id.as_str())?; Ok(self .base_contract .approve(deps, env, info, spender, token_id, expires)?) } - fn revoke( + pub fn revoke_include_nft_expired( &self, deps: DepsMut, env: Env, info: MessageInfo, spender: String, token_id: String, - ) -> Result, ContractError> { - self.assert_valid_nft(deps.as_ref(), &env, &token_id)?; + ) -> Result, ContractError> { + self.assert_nft_expired(deps.as_ref(), &env, token_id.as_str())?; Ok(self .base_contract .revoke(deps, env, info, spender, token_id)?) } - fn approve_all( + pub fn transfer_nft_include_nft_expired( &self, deps: DepsMut, env: Env, info: MessageInfo, - operator: String, - expires: Option, - ) -> Result { + recipient: String, + token_id: String, + ) -> Result, ContractError> { + self.assert_nft_expired(deps.as_ref(), &env, token_id.as_str())?; Ok(self .base_contract - .approve_all(deps, env, info, operator, expires)?) + .transfer_nft(deps, env, info, recipient, token_id)?) } - fn revoke_all( + pub fn send_nft_include_nft_expired( &self, deps: DepsMut, env: Env, info: MessageInfo, - operator: String, - ) -> Result { - Ok(self.base_contract.revoke_all(deps, env, info, operator)?) + contract: String, + token_id: String, + msg: Binary, + ) -> Result, ContractError> { + self.assert_nft_expired(deps.as_ref(), &env, token_id.as_str())?; + Ok(self + .base_contract + .send_nft(deps, env, info, contract, token_id, msg)?) } - fn burn( + pub fn burn_nft_include_nft_expired( &self, deps: DepsMut, env: Env, info: MessageInfo, token_id: String, - ) -> Result { - self.assert_valid_nft(deps.as_ref(), &env, &token_id)?; - Ok(self.base_contract.burn(deps, env, info, token_id)?) - } -} - -// helpers -impl<'a> Cw721ExpirationContract<'a> { - /// throws contract error if nft is expired - pub fn is_valid_nft(&self, deps: Deps, env: &Env, token_id: &str) -> StdResult { - // any non-expired token approval can send - let mint_date = self.mint_timestamps.load(deps.storage, token_id)?; - let expiration_days = self.expiration_days.load(deps.storage)?; - let expiration = mint_date.plus_days(expiration_days.into()); - if env.block.time >= expiration { - return Ok(false); - } - Ok(true) - } - - /// throws contract error if nft is expired - pub fn assert_valid_nft( - &self, - deps: Deps, - env: &Env, - token_id: &str, - ) -> Result<(), ContractError> { - // any non-expired token approval can send - let mint_date = self.mint_timestamps.load(deps.storage, token_id)?; - let expiration_days = self.expiration_days.load(deps.storage)?; - let expiration = mint_date.plus_days(expiration_days.into()); - if env.block.time >= expiration { - return Err(ContractError::NftExpired { - token_id: token_id.to_string(), - mint_date, - expiration, - }); - } - Ok(()) + ) -> Result, ContractError> { + self.assert_nft_expired(deps.as_ref(), &env, token_id.as_str())?; + Ok(self.base_contract.burn_nft(deps, env, info, token_id)?) } } diff --git a/contracts/cw721-expiration/src/lib.rs b/contracts/cw721-expiration/src/lib.rs index c841de969..c7c35261a 100644 --- a/contracts/cw721-expiration/src/lib.rs +++ b/contracts/cw721-expiration/src/lib.rs @@ -8,20 +8,20 @@ pub mod state; mod contract_tests; use cosmwasm_std::Empty; +use cw721::state::DefaultOptionMetadataExtension; // Version info for migration const CONTRACT_NAME: &str = "crates.io:cw721-expiration"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub type MinterResponse = cw721::MinterResponse; -pub type Extension = Option; +pub type MinterResponse = cw721::msg::MinterResponse; -pub type TokenInfo = cw721_base::state::TokenInfo; +pub type NftInfo = cw721::state::NftInfo; pub mod entry { use crate::{ error::ContractError, - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + msg::{InstantiateMsg, QueryMsg}, state::Cw721ExpirationContract, }; @@ -30,17 +30,20 @@ pub mod entry { #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response}; + use cw721::{msg::Cw721ExecuteMsg, state::DefaultOptionMetadataExtension}; // This makes a conscious choice on the various generics used by the contract #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - Cw721ExpirationContract::default().instantiate(deps.branch(), env, info, msg) + let contract = + Cw721ExpirationContract::::default( + ); + contract.instantiate(deps, env, info, msg) } #[entry_point] @@ -48,19 +51,30 @@ pub mod entry { deps: DepsMut, env: Env, info: MessageInfo, - msg: ExecuteMsg, + msg: Cw721ExecuteMsg, ) -> Result { - Cw721ExpirationContract::default().execute(deps, env, info, msg) + let contract = + Cw721ExpirationContract::::default( + ); + contract.execute(deps, env, info, msg) } #[entry_point] - pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { - Cw721ExpirationContract::default().query(deps, env, msg) + pub fn query( + deps: Deps, + env: Env, + msg: QueryMsg, + ) -> Result { + let contract = + Cw721ExpirationContract::::default( + ); + contract.query(deps, env, msg) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> Result { - Ok(Response::default()) + // TODO: allow migration e.g. from cw721-base + panic!("This contract does not support migrations") } } @@ -84,9 +98,9 @@ mod tests { mock_info("mrt", &[]), InstantiateMsg { expiration_days: 0, - name: "".into(), - symbol: "".into(), - minter: Some("mrt".into()), + name: "collection_name".into(), + symbol: "collection_symbol".into(), + minter: Some("minter".into()), withdraw_address: None, }, ) @@ -102,7 +116,7 @@ mod tests { expiration_days: 1, name: "".into(), symbol: "".into(), - minter: Some("mrt".into()), + minter: Some("minter".into()), withdraw_address: None, }, ) @@ -119,7 +133,7 @@ mod tests { assert_eq!( 1, - Cw721ExpirationContract::default() + Cw721ExpirationContract::::default() .expiration_days .load(deps.as_ref().storage) .unwrap() diff --git a/contracts/cw721-expiration/src/msg.rs b/contracts/cw721-expiration/src/msg.rs index 5aa019922..017b90699 100644 --- a/contracts/cw721-expiration/src/msg.rs +++ b/contracts/cw721-expiration/src/msg.rs @@ -1,8 +1,11 @@ -use crate::{Extension, MinterResponse}; +use crate::DefaultOptionMetadataExtension; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Empty; -use cw_ownable::cw_ownable_query; -pub type ExecuteMsg = cw721_base::ExecuteMsg; +use cosmwasm_std::Addr; +use cw721::state::CollectionInfo; +use cw_ownable::Ownership; + +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::msg::{Cw721ExecuteMsg as ExecuteMsg, Cw721MigrateMsg as MigrateMsg, *}; #[cw_serde] pub struct InstantiateMsg { @@ -23,100 +26,114 @@ pub struct InstantiateMsg { pub withdraw_address: Option, } -#[cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg { +pub enum QueryMsg { + // -------- below adds `include_expired_nft` prop to cw721/src/msg.rs -------- /// Return the owner of the given token, error if token does not exist - #[returns(cw721::OwnerOfResponse)] + #[returns(cw721::msg::OwnerOfResponse)] OwnerOf { token_id: String, /// unset or false will filter out expired approvals, you must set to true to see them include_expired: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, }, /// Return operator that can access all of the owner's tokens. - #[returns(cw721::ApprovalResponse)] + #[returns(cw721::msg::ApprovalResponse)] Approval { token_id: String, spender: String, include_expired: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, }, /// Return approvals that a token has - #[returns(cw721::ApprovalsResponse)] + #[returns(cw721::msg::ApprovalsResponse)] Approvals { token_id: String, include_expired: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, - }, - /// Return approval of a given operator for all tokens of an owner, error if not set - #[returns(cw721::OperatorResponse)] - Operator { - owner: String, - operator: String, - include_expired: Option, - }, - /// List all operators that can access all of the owner's tokens - #[returns(cw721::OperatorsResponse)] - AllOperators { - owner: String, - /// unset or false will filter out expired items, you must set to true to see them - include_expired: Option, - start_after: Option, - limit: Option, + include_expired_nft: Option, }, - /// Total number of tokens issued, including all expired NFTs - #[returns(cw721::NumTokensResponse)] - NumTokens {}, - /// With MetaData Extension. - /// Returns top-level metadata about the contract - #[returns(cw721::ContractInfoResponse)] - ContractInfo {}, /// With MetaData Extension. /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* /// but directly from the contract - #[returns(cw721::NftInfoResponse)] + #[returns(cw721::msg::NftInfoResponse)] NftInfo { token_id: String, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, }, + /// With MetaData Extension. /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization /// for clients - #[returns(cw721::AllNftInfoResponse)] + #[returns(cw721::msg::AllNftInfoResponse)] AllNftInfo { token_id: String, /// unset or false will filter out expired approvals, you must set to true to see them include_expired: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, }, /// With Enumerable extension. /// Returns all tokens owned by the given address, [] if unset. - #[returns(cw721::TokensResponse)] + #[returns(cw721::msg::TokensResponse)] Tokens { owner: String, start_after: Option, limit: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, }, + /// With Enumerable extension. /// Requires pagination. Lists all token_ids controlled by the contract. - #[returns(cw721::TokensResponse)] + #[returns(cw721::msg::TokensResponse)] AllTokens { start_after: Option, limit: Option, /// unset or false will filter out expired nfts, you must set to true to see them - include_invalid: Option, + include_expired_nft: Option, + }, + + // -------- below is from cw721/src/msg.rs -------- + /// Return approval of a given operator for all tokens of an owner, error if not set + #[returns(cw721::msg::OperatorResponse)] + Operator { + owner: String, + operator: String, + include_expired: Option, + }, + /// List all operators that can access all of the owner's tokens + #[returns(cw721::msg::OperatorsResponse)] + AllOperators { + owner: String, + /// unset or false will filter out expired items, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, }, + /// Total number of tokens issued, including all expired NFTs + #[returns(cw721::msg::NumTokensResponse)] + NumTokens {}, + + #[returns(cw721::state::CollectionInfo)] + ContractInfo {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract + #[returns(CollectionInfo)] + GetCollectionInfo {}, + + #[returns(Ownership)] + Ownership {}, + + #[returns(Ownership)] + GetMinterOwnership {}, /// Return the minter #[returns(MinterResponse)] @@ -124,5 +141,11 @@ pub enum QueryMsg { /// Extension query #[returns(())] - Extension { msg: Empty }, + Extension { + msg: TQueryExtensionMsg, + phantom: Option, // dummy field to infer type + }, + + #[returns(Option)] + GetWithdrawAddress {}, } diff --git a/contracts/cw721-expiration/src/query.rs b/contracts/cw721-expiration/src/query.rs index a7d7626dc..c34e560a8 100644 --- a/contracts/cw721-expiration/src/query.rs +++ b/contracts/cw721-expiration/src/query.rs @@ -1,65 +1,153 @@ -use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, StdResult}; -use cw721::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721Query, - MinterResponse, NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, - OwnerOfResponse, TokensResponse, +use cosmwasm_std::{to_json_binary, Binary, CustomMsg, Deps, Env, StdResult}; +use cw721::msg::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, NftInfoResponse, OwnerOfResponse, + TokensResponse, }; +use cw721::query::Cw721Query; +use serde::de::DeserializeOwned; +use serde::Serialize; -use crate::{error::ContractError, msg::QueryMsg, state::Cw721ExpirationContract, Extension}; +use crate::{error::ContractError, msg::QueryMsg, state::Cw721ExpirationContract}; -impl<'a> Cw721ExpirationContract<'a> { - pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> Result { +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721ExpirationContract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + pub fn query( + &self, + deps: Deps, + env: Env, + msg: QueryMsg, + ) -> Result { + let contract = Cw721ExpirationContract::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); match msg { - QueryMsg::Minter {} => Ok(to_json_binary(&self.minter(deps)?)?), - QueryMsg::ContractInfo {} => Ok(to_json_binary(&self.contract_info(deps)?)?), - QueryMsg::NftInfo { + // -------- msgs with `include_expired_nft` prop -------- + QueryMsg::OwnerOf { token_id, - include_invalid, - } => Ok(to_json_binary(&self.nft_info( - deps, - env, + include_expired: include_expired_approval, + include_expired_nft, + } => Ok(to_json_binary( + &contract.query_owner_of_include_expired_nft( + deps, + env, + token_id, + include_expired_approval.unwrap_or(false), + include_expired_nft.unwrap_or(false), + )?, + )?), + QueryMsg::Approval { token_id, - include_invalid.unwrap_or(false), - )?)?), - QueryMsg::OwnerOf { + spender, + include_expired, + include_expired_nft: include_invalid, + } => Ok(to_json_binary( + &contract.query_approval_include_expired_nft( + deps, + env, + token_id, + spender, + include_expired.unwrap_or(false), + include_invalid.unwrap_or(false), + )?, + )?), + QueryMsg::Approvals { token_id, include_expired, - include_invalid, - } => Ok(to_json_binary(&self.owner_of( - deps, - env, + include_expired_nft: include_invalid, + } => Ok(to_json_binary( + &contract.query_approvals_include_expired_nft( + deps, + env, + token_id, + include_expired.unwrap_or(false), + include_invalid.unwrap_or(false), + )?, + )?), + QueryMsg::NftInfo { token_id, - include_expired.unwrap_or(false), - include_invalid.unwrap_or(false), - )?)?), + include_expired_nft: include_expired, + } => Ok(to_json_binary( + &contract.query_nft_info_include_expired_nft( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?, + )?), QueryMsg::AllNftInfo { token_id, - include_expired, - include_invalid, - } => Ok(to_json_binary(&self.all_nft_info( - deps, - env, - token_id, - include_expired.unwrap_or(false), - include_invalid.unwrap_or(false), - )?)?), + include_expired: include_expired_approval, + include_expired_nft, + } => Ok(to_json_binary( + &contract.query_all_nft_info_include_expired_nft( + deps, + env, + token_id, + include_expired_approval.unwrap_or(false), + include_expired_nft.unwrap_or(false), + )?, + )?), + QueryMsg::Tokens { + owner, + start_after, + limit, + include_expired_nft: include_invalid, + } => Ok(to_json_binary( + &contract.query_tokens_include_expired_nft( + deps, + env, + owner, + start_after, + limit, + include_invalid.unwrap_or(false), + )?, + )?), + QueryMsg::AllTokens { + start_after, + limit, + include_expired_nft: include_invalid, + } => Ok(to_json_binary( + &contract.query_all_tokens_include_expired_nft( + deps, + env, + start_after, + limit, + include_invalid.unwrap_or(false), + )?, + )?), + // -------- below is from cw721/src/msg.rs -------- QueryMsg::Operator { owner, operator, - include_expired, - } => Ok(to_json_binary(&self.operator( + include_expired: include_expired_approval, + } => Ok(to_json_binary(&contract.base_contract.query_operator( deps, env, owner, operator, - include_expired.unwrap_or(false), + include_expired_approval.unwrap_or(false), )?)?), QueryMsg::AllOperators { owner, include_expired, start_after, limit, - } => Ok(to_json_binary(&self.operators( + } => Ok(to_json_binary(&contract.base_contract.query_operators( deps, env, owner, @@ -67,229 +155,201 @@ impl<'a> Cw721ExpirationContract<'a> { start_after, limit, )?)?), - QueryMsg::NumTokens {} => Ok(to_json_binary(&self.num_tokens(deps)?)?), - QueryMsg::Tokens { - owner, - start_after, - limit, - include_invalid, - } => Ok(to_json_binary(&self.tokens( - deps, - env, - owner, - start_after, - limit, - include_invalid.unwrap_or(false), - )?)?), - QueryMsg::AllTokens { - start_after, - limit, - include_invalid, - } => Ok(to_json_binary(&self.all_tokens( - deps, - env, - start_after, - limit, - include_invalid.unwrap_or(false), - )?)?), - QueryMsg::Approval { - token_id, - spender, - include_expired, - include_invalid, - } => Ok(to_json_binary(&self.approval( - deps, - env, - token_id, - spender, - include_expired.unwrap_or(false), - include_invalid.unwrap_or(false), - )?)?), - QueryMsg::Approvals { - token_id, - include_expired, - include_invalid, - } => Ok(to_json_binary(&self.approvals( - deps, - env, - token_id, - include_expired.unwrap_or(false), - include_invalid.unwrap_or(false), - )?)?), - QueryMsg::Ownership {} => Ok(to_json_binary(&Self::ownership(deps)?)?), - QueryMsg::Extension { msg: _ } => Ok(Binary::default()), + QueryMsg::NumTokens {} => Ok(to_json_binary( + &contract.base_contract.query_num_tokens(deps, env)?, + )?), + QueryMsg::ContractInfo {} => Ok(to_json_binary( + &contract.base_contract.query_collection_info(deps, env)?, + )?), + QueryMsg::GetCollectionInfo {} => Ok(to_json_binary( + &contract.base_contract.query_collection_info(deps, env)?, + )?), + QueryMsg::Ownership {} => Ok(to_json_binary( + &contract + .base_contract + .query_minter_ownership(deps.storage)?, + )?), + QueryMsg::GetMinterOwnership {} => Ok(to_json_binary( + &contract + .base_contract + .query_minter_ownership(deps.storage)?, + )?), + QueryMsg::Minter {} => Ok(to_json_binary( + &contract.base_contract.query_minter(deps.storage)?, + )?), + QueryMsg::Extension { msg, .. } => Ok(to_json_binary( + &contract.base_contract.query_extension(deps, env, msg)?, + )?), + QueryMsg::GetWithdrawAddress {} => Ok(to_json_binary( + &contract.base_contract.query_withdraw_address(deps)?, + )?), } } - pub fn minter(&self, deps: Deps) -> StdResult { - self.base_contract.minter(deps) - } - - pub fn ownership(deps: Deps) -> StdResult> { - cw_ownable::get_ownership(deps.storage) - } -} - -// queries -impl<'a> Cw721ExpirationContract<'a> { - pub fn contract_info(&self, deps: Deps) -> StdResult { - self.base_contract.contract_info(deps) - } - - pub fn num_tokens(&self, deps: Deps) -> StdResult { - self.base_contract.num_tokens(deps) - } - - pub fn nft_info( + pub fn query_nft_info_include_expired_nft( &self, deps: Deps, env: Env, token_id: String, - include_invalid: bool, - ) -> Result, ContractError> { - if !include_invalid { - self.assert_valid_nft(deps, &env, token_id.as_str())?; + include_expired_nft: bool, + ) -> Result, ContractError> { + if !include_expired_nft { + self.assert_nft_expired(deps, &env, token_id.as_str())?; } - Ok(self.base_contract.nft_info(deps, token_id)?) + Ok(self.base_contract.query_nft_info(deps, env, token_id)?) } - pub fn owner_of( + pub fn query_owner_of_include_expired_nft( &self, deps: Deps, env: Env, token_id: String, - include_expired: bool, - include_invalid: bool, + include_expired_approval: bool, + include_expired_nft: bool, ) -> Result { - if !include_invalid { - self.assert_valid_nft(deps, &env, token_id.as_str())?; + if !include_expired_nft { + self.assert_nft_expired(deps, &env, token_id.as_str())?; } Ok(self .base_contract - .owner_of(deps, env, token_id, include_expired)?) + .query_owner_of(deps, env, token_id, include_expired_approval)?) } - /// operator returns the approval status of an operator for a given owner if exists - pub fn operator( - &self, - deps: Deps, - env: Env, - owner: String, - operator: String, - include_expired: bool, - ) -> StdResult { - self.base_contract - .operator(deps, env, owner, operator, include_expired) - } - - /// operators returns all operators owner given access to - pub fn operators( - &self, - deps: Deps, - env: Env, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult { - self.base_contract - .operators(deps, env, owner, include_expired, start_after, limit) - } - - pub fn approval( + pub fn query_approval_include_expired_nft( &self, deps: Deps, env: Env, token_id: String, spender: String, - include_expired: bool, - include_invalid: bool, + include_expired_approval: bool, + include_expired_nft: bool, ) -> Result { - if !include_invalid { - self.assert_valid_nft(deps, &env, token_id.as_str())?; + if !include_expired_nft { + self.assert_nft_expired(deps, &env, token_id.as_str())?; } - Ok(self - .base_contract - .approval(deps, env, token_id, spender, include_expired)?) + Ok(self.base_contract.query_approval( + deps, + env, + token_id, + spender, + include_expired_approval, + )?) } /// approvals returns all approvals owner given access to - pub fn approvals( + pub fn query_approvals_include_expired_nft( &self, deps: Deps, env: Env, token_id: String, - include_expired: bool, - include_invalid: bool, + include_expired_approval: bool, + include_expired_nft: bool, ) -> Result { - if !include_invalid { - self.assert_valid_nft(deps, &env, token_id.as_str())?; + if !include_expired_nft { + self.assert_nft_expired(deps, &env, token_id.as_str())?; } Ok(self .base_contract - .approvals(deps, env, token_id, include_expired)?) + .query_approvals(deps, env, token_id, include_expired_approval)?) } - pub fn tokens( + pub fn query_tokens_include_expired_nft( &self, deps: Deps, env: Env, owner: String, start_after: Option, limit: Option, - include_invalid: bool, + include_expired_nft: bool, ) -> StdResult { - let tokens = self.base_contract.tokens(deps, owner, start_after, limit)?; - if include_invalid { + let tokens = + self.base_contract + .query_tokens(deps, env.clone(), owner, start_after, limit)?; + if include_expired_nft { return Ok(tokens); } let filtered: Vec<_> = tokens .tokens .iter() .filter(|token_id| { - self.is_valid_nft(deps, &env, token_id).unwrap_or(false) // Convert Option to bool + self.is_nft_expired(deps, &env, token_id).unwrap_or(false) // Convert Option to bool }) .map(|token_id| token_id.to_string()) .collect(); Ok(TokensResponse { tokens: filtered }) } - pub fn all_tokens( + pub fn query_all_tokens_include_expired_nft( &self, deps: Deps, env: Env, start_after: Option, limit: Option, - include_invalid: bool, + include_expired_nft: bool, ) -> Result { - let tokens = self.base_contract.all_tokens(deps, start_after, limit)?; - if include_invalid { + let tokens = self + .base_contract + .query_all_tokens(deps, env.clone(), start_after, limit)?; + if include_expired_nft { return Ok(tokens); } let filtered: Vec<_> = tokens .tokens .iter() .filter(|token_id| { - self.is_valid_nft(deps, &env, token_id).unwrap_or(false) // Convert Option to bool + self.is_nft_expired(deps, &env, token_id).unwrap_or(false) // Convert Option to bool }) .map(|token_id| token_id.to_string()) .collect(); Ok(TokensResponse { tokens: filtered }) } - pub fn all_nft_info( + pub fn query_all_nft_info_include_expired_nft( &self, deps: Deps, env: Env, token_id: String, - include_expired: bool, - include_invalid: bool, - ) -> Result, ContractError> { - if !include_invalid { - self.assert_valid_nft(deps, &env, token_id.as_str())?; + include_expired_approval: bool, + include_expired_nft: bool, + ) -> Result, ContractError> { + if !include_expired_nft { + self.assert_nft_expired(deps, &env, token_id.as_str())?; } Ok(self .base_contract - .all_nft_info(deps, env, token_id, include_expired)?) + .query_all_nft_info(deps, env, token_id, include_expired_approval)?) + } + + // --- helpers --- + pub fn is_nft_expired(&self, deps: Deps, env: &Env, token_id: &str) -> StdResult { + // any non-expired token approval can send + let mint_date = self.mint_timestamps.load(deps.storage, token_id)?; + let expiration_days = self.expiration_days.load(deps.storage)?; + let expiration = mint_date.plus_days(expiration_days.into()); + if env.block.time >= expiration { + return Ok(false); + } + Ok(true) + } + + /// throws contract error if nft is expired + pub fn assert_nft_expired( + &self, + deps: Deps, + env: &Env, + token_id: &str, + ) -> Result<(), ContractError> { + // any non-expired token approval can send + let mint_date = self.mint_timestamps.load(deps.storage, token_id)?; + let expiration_days = self.expiration_days.load(deps.storage)?; + let expiration = mint_date.plus_days(expiration_days.into()); + if env.block.time >= expiration { + return Err(ContractError::NftExpired { + token_id: token_id.to_string(), + mint_date, + expiration, + }); + } + Ok(()) } } diff --git a/contracts/cw721-expiration/src/state.rs b/contracts/cw721-expiration/src/state.rs index c7ccb3d0d..ebde17ad3 100644 --- a/contracts/cw721-expiration/src/state.rs +++ b/contracts/cw721-expiration/src/state.rs @@ -1,20 +1,57 @@ -use cosmwasm_std::{Empty, Timestamp}; -use cw_storage_plus::{Item, Map}; +use cosmwasm_std::{CustomMsg, Timestamp}; + +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::state::*; -use crate::Extension; +use cw721_base::Cw721Contract; +use cw_storage_plus::{Item, Map}; +use serde::de::DeserializeOwned; +use serde::Serialize; -pub struct Cw721ExpirationContract<'a> { +pub struct Cw721ExpirationContract< + 'a, + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ pub expiration_days: Item<'a, u16>, // max 65535 days pub mint_timestamps: Map<'a, &'a str, Timestamp>, - pub base_contract: cw721_base::Cw721Contract<'a, Extension, Empty, Empty, Empty>, + pub base_contract: Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >, } -impl Default for Cw721ExpirationContract<'static> { +impl Default + for Cw721ExpirationContract< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ fn default() -> Self { Self { expiration_days: Item::new("expiration_days"), mint_timestamps: Map::new("mint_timestamps"), - base_contract: cw721_base::Cw721Contract::default(), + base_contract: Cw721Contract::default(), } } } diff --git a/contracts/cw721-fixed-price/Cargo.toml b/contracts/cw721-fixed-price/Cargo.toml index f47aeddb7..82f018e28 100644 --- a/contracts/cw721-fixed-price/Cargo.toml +++ b/contracts/cw721-fixed-price/Cargo.toml @@ -22,6 +22,7 @@ cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } +cw721 = { workspace = true } cw721-base = { workspace = true, features = ["library"] } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } diff --git a/contracts/cw721-fixed-price/README.md b/contracts/cw721-fixed-price/README.md index af3256fc2..58baabc32 100644 --- a/contracts/cw721-fixed-price/README.md +++ b/contracts/cw721-fixed-price/README.md @@ -25,7 +25,7 @@ To generate an optimized build run: docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/rust-optimizer:0.12.3 + cosmwasm/rust-optimizer:0.16.0 ``` ### Testing diff --git a/contracts/cw721-fixed-price/schema/cw721-fixed-price.json b/contracts/cw721-fixed-price/schema/cw721-fixed-price.json index 694ec7f85..296c736a8 100644 --- a/contracts/cw721-fixed-price/schema/cw721-fixed-price.json +++ b/contracts/cw721-fixed-price/schema/cw721-fixed-price.json @@ -1,6 +1,6 @@ { "contract_name": "cw721-fixed-price", - "contract_version": "0.18.0", + "contract_version": "0.19.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -23,7 +23,7 @@ "extension": { "anyOf": [ { - "$ref": "#/definitions/Empty" + "$ref": "#/definitions/Metadata" }, { "type": "null" @@ -68,9 +68,90 @@ "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" }, - "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" + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -180,7 +261,7 @@ "extension": { "anyOf": [ { - "$ref": "#/definitions/Empty" + "$ref": "#/definitions/Metadata" }, { "type": "null" @@ -219,9 +300,90 @@ "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" }, - "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" + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", diff --git a/contracts/cw721-fixed-price/src/contract.rs b/contracts/cw721-fixed-price/src/contract.rs index 1b9d7a4f4..eea581249 100644 --- a/contracts/cw721-fixed-price/src/contract.rs +++ b/contracts/cw721-fixed-price/src/contract.rs @@ -11,10 +11,9 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; use cw20::Cw20ReceiveMsg; -use cw721_base::{ - helpers::Cw721Contract, msg::ExecuteMsg as Cw721ExecuteMsg, - msg::InstantiateMsg as Cw721InstantiateMsg, -}; +use cw721::helpers::Cw721Contract; +use cw721::msg::{Cw721ExecuteMsg, Cw721InstantiateMsg}; +use cw721::state::DefaultOptionMetadataExtension; use cw_utils::parse_reply_instantiate_data; // version info for migration info @@ -160,7 +159,7 @@ pub fn execute_receive( return Err(ContractError::WrongPaymentAmount {}); } - let mint_msg = Cw721ExecuteMsg::<_, Empty>::Mint { + let mint_msg = Cw721ExecuteMsg::::Mint { token_id: config.unused_token_id.to_string(), owner: sender, token_uri: config.token_uri.clone().into(), @@ -169,8 +168,13 @@ pub fn execute_receive( match config.cw721_address.clone() { Some(cw721) => { - let callback = - Cw721Contract::(cw721, PhantomData, PhantomData).call(mint_msg)?; + let callback = Cw721Contract::( + cw721, + PhantomData, + PhantomData, + PhantomData, + ) + .call(mint_msg)?; config.unused_token_id += 1; CONFIG.save(deps.storage, &config)?; @@ -185,7 +189,7 @@ mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{from_json, to_json_binary, CosmosMsg, SubMsgResponse, SubMsgResult}; - use cw721_base::Extension; + use cw721::state::DefaultOptionMetadataExtension; use prost::Message; const NFT_CONTRACT_ADDR: &str = "nftcontract"; @@ -378,7 +382,7 @@ mod tests { let info = mock_info(MOCK_CONTRACT_ADDR, &[]); let res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - let mint_msg = Cw721ExecuteMsg::::Mint { + let mint_msg = Cw721ExecuteMsg::::Mint { token_id: String::from("0"), owner: String::from("minter"), token_uri: Some(String::from("https://ipfs.io/ipfs/Q")), diff --git a/contracts/cw721-fixed-price/src/msg.rs b/contracts/cw721-fixed-price/src/msg.rs index cdf723854..ae9745b43 100644 --- a/contracts/cw721-fixed-price/src/msg.rs +++ b/contracts/cw721-fixed-price/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint128}; use cw20::Cw20ReceiveMsg; -use cw721_base::Extension; +use cw721::state::DefaultOptionMetadataExtension; #[cw_serde] pub struct InstantiateMsg { @@ -13,7 +13,7 @@ pub struct InstantiateMsg { pub token_code_id: u64, pub cw20_address: Addr, pub token_uri: String, - pub extension: Extension, + pub extension: DefaultOptionMetadataExtension, pub withdraw_address: Option, } @@ -39,6 +39,6 @@ pub struct ConfigResponse { pub name: String, pub symbol: String, pub token_uri: String, - pub extension: Extension, + pub extension: DefaultOptionMetadataExtension, pub unused_token_id: u32, } diff --git a/contracts/cw721-fixed-price/src/state.rs b/contracts/cw721-fixed-price/src/state.rs index a1ad80c99..9179a8cd5 100644 --- a/contracts/cw721-fixed-price/src/state.rs +++ b/contracts/cw721-fixed-price/src/state.rs @@ -1,6 +1,9 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; -use cw721_base::Extension; + +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::state::*; + use cw_storage_plus::Item; #[cw_serde] @@ -13,7 +16,7 @@ pub struct Config { pub name: String, pub symbol: String, pub token_uri: String, - pub extension: Extension, + pub extension: DefaultOptionMetadataExtension, pub unused_token_id: u32, } diff --git a/contracts/cw721-metadata-onchain/.cargo/config b/contracts/cw721-metadata-onchain/.cargo/config deleted file mode 100644 index 7d1a066c8..000000000 --- a/contracts/cw721-metadata-onchain/.cargo/config +++ /dev/null @@ -1,5 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -wasm-debug = "build --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example schema" diff --git a/contracts/cw721-metadata-onchain/Cargo.toml b/contracts/cw721-metadata-onchain/Cargo.toml deleted file mode 100644 index 75944e575..000000000 --- a/contracts/cw721-metadata-onchain/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "cw721-metadata-onchain" -description = "Example extending CW721 NFT to store metadata on chain" -authors = [ - "Ethan Frey ", - "Orkun Külçe ", -] -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -documentation = { workspace = true } - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[dependencies] -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true } -cw2 = { workspace = true } -cw721 = { workspace = true } -cw721-base = { workspace = true, features = ["library"] } -schemars = { workspace = true } -serde = { workspace = true } diff --git a/contracts/cw721-metadata-onchain/NOTICE b/contracts/cw721-metadata-onchain/NOTICE deleted file mode 100644 index bd298d741..000000000 --- a/contracts/cw721-metadata-onchain/NOTICE +++ /dev/null @@ -1,14 +0,0 @@ -Cw721_metadata_onchain -Copyright (C) 2021 Confio OÜ - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/contracts/cw721-metadata-onchain/README.md b/contracts/cw721-metadata-onchain/README.md deleted file mode 100644 index dd449656e..000000000 --- a/contracts/cw721-metadata-onchain/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# CW721 Metadata Onchain - -NFT creators may want to store their NFT metadata on-chain so other contracts are able to interact with it. -With CW721-Base in CosmWasm, we allow you to store any data on chain you wish, using a generic `extension: T`. - -In order to support on-chain metadata, and to demonstrate how to use the extension ability, we have created this simple contract. -There is no business logic here, but looking at `lib.rs` will show you how do define custom data that is included when minting and -available in all queries. - -In particular, here we define: - -```rust -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct Trait { - pub display_type: Option, - pub trait_type: String, - pub value: String, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct Metadata { - pub image: Option, - pub image_data: Option, - pub external_url: Option, - pub description: Option, - pub name: Option, - pub attributes: Option>, - pub background_color: Option, - pub animation_url: Option, - pub youtube_url: Option, -} - -pub type Extension = Option; -``` - -In particular, the fields defined conform to the properties supported in the [OpenSea Metadata Standard](https://docs.opensea.io/docs/metadata-standards). - - -This means when you query `NftInfo{name: "Enterprise"}`, you will get something like: - -```json -{ - "name": "Enterprise", - "token_uri": "https://starships.example.com/Starship/Enterprise.json", - "extension": { - "image": null, - "image_data": null, - "external_url": null, - "description": "Spaceship with Warp Drive", - "name": "Starship USS Enterprise", - "attributes": null, - "background_color": null, - "animation_url": null, - "youtube_url": null - } -} -``` - -Please look at the test code for an example usage in Rust. - -## Notice - -Feel free to use this contract out of the box, or as inspiration for further customization of cw721-base. -We will not be adding new features or business logic here. diff --git a/contracts/cw721-metadata-onchain/examples/schema.rs b/contracts/cw721-metadata-onchain/examples/schema.rs deleted file mode 100644 index 36087d75d..000000000 --- a/contracts/cw721-metadata-onchain/examples/schema.rs +++ /dev/null @@ -1,11 +0,0 @@ -use cosmwasm_schema::write_api; - -use cw721_metadata_onchain::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } -} diff --git a/contracts/cw721-metadata-onchain/schema/cw721-metadata-onchain.json b/contracts/cw721-metadata-onchain/schema/cw721-metadata-onchain.json deleted file mode 100644 index c6dfa6cd1..000000000 --- a/contracts/cw721-metadata-onchain/schema/cw721-metadata-onchain.json +++ /dev/null @@ -1,1875 +0,0 @@ -{ - "contract_name": "cw721-metadata-onchain", - "contract_version": "0.18.0", - "idl_version": "1.0.0", - "instantiate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "InstantiateMsg", - "type": "object", - "required": [ - "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", - "null" - ] - }, - "name": { - "description": "Name of the NFT contract", - "type": "string" - }, - "symbol": { - "description": "Symbol of the NFT contract", - "type": "string" - }, - "withdraw_address": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "execute": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ExecuteMsg", - "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", - "oneOf": [ - { - "description": "Transfer is a base message to move a token to another account without triggering actions", - "type": "object", - "required": [ - "transfer_nft" - ], - "properties": { - "transfer_nft": { - "type": "object", - "required": [ - "recipient", - "token_id" - ], - "properties": { - "recipient": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted Approval", - "type": "object", - "required": [ - "revoke" - ], - "properties": { - "revoke": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted ApproveAll permission", - "type": "object", - "required": [ - "revoke_all" - ], - "properties": { - "revoke_all": { - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Mint a new NFT, can only be called by the contract minter", - "type": "object", - "required": [ - "mint" - ], - "properties": { - "mint": { - "type": "object", - "required": [ - "owner", - "token_id" - ], - "properties": { - "extension": { - "description": "Any custom extension used by this contract", - "anyOf": [ - { - "$ref": "#/definitions/Metadata" - }, - { - "type": "null" - } - ] - }, - "owner": { - "description": "The owner of the newly minter NFT", - "type": "string" - }, - "token_id": { - "description": "Unique ID of the NFT", - "type": "string" - }, - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Burn an NFT the sender has access to", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension msg", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets address to send withdrawn fees to. Only owner can call this.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", - "type": "object", - "required": [ - "remove_withdraw_address" - ], - "properties": { - "remove_withdraw_address": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", - "type": "object", - "required": [ - "withdraw_funds" - ], - "properties": { - "withdraw_funds": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", - "type": "object", - "required": [ - "update_ownership" - ], - "properties": { - "update_ownership": { - "$ref": "#/definitions/Action" - } - }, - "additionalProperties": false - } - ], - "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" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "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 - } - ] - }, - "Metadata": { - "type": "object", - "properties": { - "animation_url": { - "type": [ - "string", - "null" - ] - }, - "attributes": { - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Trait" - } - }, - "background_color": { - "type": [ - "string", - "null" - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "external_url": { - "type": [ - "string", - "null" - ] - }, - "image": { - "type": [ - "string", - "null" - ] - }, - "image_data": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": [ - "string", - "null" - ] - }, - "youtube_url": { - "type": [ - "string", - "null" - ] - } - }, - "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" - } - ] - }, - "Trait": { - "type": "object", - "required": [ - "trait_type", - "value" - ], - "properties": { - "display_type": { - "type": [ - "string", - "null" - ] - }, - "trait_type": { - "type": "string" - }, - "value": { - "type": "string" - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - }, - "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", - "oneOf": [ - { - "description": "Return the owner of the given token, error if token does not exist", - "type": "object", - "required": [ - "owner_of" - ], - "properties": { - "owner_of": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return operator that can access all of the owner's tokens.", - "type": "object", - "required": [ - "approval" - ], - "properties": { - "approval": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approvals that a token has", - "type": "object", - "required": [ - "approvals" - ], - "properties": { - "approvals": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Return approval of a given operator for all tokens of an owner, error if not set", - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "object", - "required": [ - "operator", - "owner" - ], - "properties": { - "include_expired": { - "type": [ - "boolean", - "null" - ] - }, - "operator": { - "type": "string" - }, - "owner": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "List all operators that can access all of the owner's tokens", - "type": "object", - "required": [ - "all_operators" - ], - "properties": { - "all_operators": { - "type": "object", - "required": [ - "owner" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired items, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "owner": { - "type": "string" - }, - "start_after": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Total number of tokens issued", - "type": "object", - "required": [ - "num_tokens" - ], - "properties": { - "num_tokens": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns top-level metadata about the contract", - "type": "object", - "required": [ - "contract_info" - ], - "properties": { - "contract_info": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", - "type": "object", - "required": [ - "nft_info" - ], - "properties": { - "nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", - "type": "object", - "required": [ - "all_nft_info" - ], - "properties": { - "all_nft_info": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "include_expired": { - "description": "unset or false will filter out expired approvals, you must set to true to see them", - "type": [ - "boolean", - "null" - ] - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Return the minter", - "type": "object", - "required": [ - "minter" - ], - "properties": { - "minter": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Extension query", - "type": "object", - "required": [ - "extension" - ], - "properties": { - "extension": { - "type": "object", - "required": [ - "msg" - ], - "properties": { - "msg": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "get_withdraw_address" - ], - "properties": { - "get_withdraw_address": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Query the contract's ownership information", - "type": "object", - "required": [ - "ownership" - ], - "properties": { - "ownership": { - "type": "object", - "additionalProperties": false - } - }, - "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" - } - } - }, - "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_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" - } - } - }, - "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 - }, - "extension": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Null", - "type": "null" - }, - "get_withdraw_address": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Nullable_String", - "type": [ - "string", - "null" - ] - }, - "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" - } - } - }, - "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_String", - "description": "The contract's ownership info", - "type": "object", - "properties": { - "owner": { - "description": "The contract's current owner. `None` if the ownership has been renounced.", - "type": [ - "string", - "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.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "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-metadata-onchain/src/lib.rs b/contracts/cw721-metadata-onchain/src/lib.rs deleted file mode 100644 index e8a133122..000000000 --- a/contracts/cw721-metadata-onchain/src/lib.rs +++ /dev/null @@ -1,143 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::Empty; -pub use cw721_base::{ContractError, InstantiateMsg}; - -// Version info for migration -const CONTRACT_NAME: &str = "crates.io:cw721-metadata-onchain"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[cw_serde] -pub struct Trait { - pub display_type: Option, - pub trait_type: String, - pub value: String, -} - -// see: https://docs.opensea.io/docs/metadata-standards -#[cw_serde] -#[derive(Default)] -pub struct Metadata { - pub image: Option, - pub image_data: Option, - pub external_url: Option, - pub description: Option, - pub name: Option, - pub attributes: Option>, - pub background_color: Option, - pub animation_url: Option, - pub youtube_url: Option, -} - -pub type Extension = Option; - -pub type Cw721MetadataContract<'a> = cw721_base::Cw721Contract<'a, Extension, Empty, Empty, Empty>; -pub type ExecuteMsg = cw721_base::ExecuteMsg; -pub type QueryMsg = cw721_base::QueryMsg; - -#[cfg(not(feature = "library"))] -pub mod entry { - use super::*; - - use cosmwasm_std::entry_point; - use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; - - // This makes a conscious choice on the various generics used by the contract - #[entry_point] - pub fn instantiate( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, - ) -> Result { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - Cw721MetadataContract::default().instantiate(deps.branch(), env, info, msg) - } - - #[entry_point] - pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, - ) -> Result { - Cw721MetadataContract::default().execute(deps, env, info, msg) - } - - #[entry_point] - pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - Cw721MetadataContract::default().query(deps, env, msg) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cw721::Cw721Query; - - const CREATOR: &str = "creator"; - - /// Make sure cw2 version info is properly initialized during instantiation, - /// and NOT overwritten by the base contract. - #[test] - fn proper_cw2_initialization() { - let mut deps = mock_dependencies(); - - entry::instantiate( - deps.as_mut(), - mock_env(), - mock_info("larry", &[]), - InstantiateMsg { - name: "".into(), - symbol: "".into(), - minter: None, - withdraw_address: None, - }, - ) - .unwrap(); - - let version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); - assert_eq!(version.contract, CONTRACT_NAME); - assert_ne!(version.contract, cw721_base::CONTRACT_NAME); - } - - #[test] - fn use_metadata_extension() { - let mut deps = mock_dependencies(); - let contract = Cw721MetadataContract::default(); - - let info = mock_info(CREATOR, &[]); - let init_msg = InstantiateMsg { - name: "SpaceShips".to_string(), - symbol: "SPACE".to_string(), - minter: None, - withdraw_address: None, - }; - contract - .instantiate(deps.as_mut(), mock_env(), info.clone(), init_msg) - .unwrap(); - - let token_id = "Enterprise"; - let token_uri = Some("https://starships.example.com/Starship/Enterprise.json".into()); - let extension = Some(Metadata { - description: Some("Spaceship with Warp Drive".into()), - name: Some("Starship USS Enterprise".to_string()), - ..Metadata::default() - }); - let exec_msg = ExecuteMsg::Mint { - token_id: token_id.to_string(), - owner: "john".to_string(), - token_uri: token_uri.clone(), - extension: extension.clone(), - }; - contract - .execute(deps.as_mut(), mock_env(), info, exec_msg) - .unwrap(); - - let res = contract.nft_info(deps.as_ref(), token_id.into()).unwrap(); - assert_eq!(res.token_uri, token_uri); - assert_eq!(res.extension, extension); - } -} diff --git a/contracts/cw721-non-transferable/examples/schema.rs b/contracts/cw721-non-transferable/examples/schema.rs index cd1cde28c..6db9bbd51 100644 --- a/contracts/cw721-non-transferable/examples/schema.rs +++ b/contracts/cw721-non-transferable/examples/schema.rs @@ -1,14 +1,14 @@ use std::env::current_dir; use std::fs::create_dir_all; -use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; +use cosmwasm_schema::{export_schema_with_title, remove_schemas, schema_for}; -use cw721::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721ExecuteMsg, - MinterResponse, NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, - TokensResponse, +use cosmwasm_std::Empty; +use cw721::state::DefaultOptionMetadataExtension; +use cw721_non_transferable::{ + msg::{ExecuteMsg, MigrateMsg}, + InstantiateMsg, QueryMsg, }; -use cw721_non_transferable::{Extension, InstantiateMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -16,25 +16,13 @@ fn main() { create_dir_all(&out_dir).unwrap(); remove_schemas(&out_dir).unwrap(); - export_schema(&schema_for!(InstantiateMsg), &out_dir); - export_schema_with_title(&schema_for!(Cw721ExecuteMsg), &out_dir, "Cw721ExecuteMsg"); - export_schema(&schema_for!(QueryMsg), &out_dir); + // entry points - generate always with title for avoiding name suffixes like "..._empty_for_..." due to generics + export_schema_with_title(&schema_for!(InstantiateMsg), &out_dir, "InstantiateMsg"); export_schema_with_title( - &schema_for!(AllNftInfoResponse), + &schema_for!(ExecuteMsg::), &out_dir, - "AllNftInfoResponse", + "ExecuteMsg", ); - export_schema(&schema_for!(ApprovalResponse), &out_dir); - export_schema(&schema_for!(ApprovalsResponse), &out_dir); - export_schema(&schema_for!(OperatorsResponse), &out_dir); - export_schema(&schema_for!(ContractInfoResponse), &out_dir); - export_schema(&schema_for!(MinterResponse), &out_dir); - export_schema_with_title( - &schema_for!(NftInfoResponse), - &out_dir, - "NftInfoResponse", - ); - export_schema(&schema_for!(NumTokensResponse), &out_dir); - export_schema(&schema_for!(OwnerOfResponse), &out_dir); - export_schema(&schema_for!(TokensResponse), &out_dir); + export_schema_with_title(&schema_for!(QueryMsg), &out_dir, "QueryMsg"); + export_schema_with_title(&schema_for!(MigrateMsg), &out_dir, "MigrateMsg"); } diff --git a/contracts/cw721-non-transferable/schema/all_nft_info_response.json b/contracts/cw721-non-transferable/schema/all_nft_info_response.json deleted file mode 100644 index d263321e2..000000000 --- a/contracts/cw721-non-transferable/schema/all_nft_info_response.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AllNftInfoResponse", - "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_Nullable_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_Nullable_Empty": { - "type": "object", - "properties": { - "extension": { - "description": "You can add any custom metadata here when you extend cw721-base", - "anyOf": [ - { - "$ref": "#/definitions/Empty" - }, - { - "type": "null" - } - ] - }, - "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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/approval_response.json b/contracts/cw721-non-transferable/schema/approval_response.json deleted file mode 100644 index b29eab59e..000000000 --- a/contracts/cw721-non-transferable/schema/approval_response.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/approvals_response.json b/contracts/cw721-non-transferable/schema/approvals_response.json deleted file mode 100644 index 7cdac0015..000000000 --- a/contracts/cw721-non-transferable/schema/approvals_response.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/cw721_execute_msg.json b/contracts/cw721-non-transferable/schema/cw721_execute_msg.json deleted file mode 100644 index 228b5c249..000000000 --- a/contracts/cw721-non-transferable/schema/cw721_execute_msg.json +++ /dev/null @@ -1,265 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Cw721ExecuteMsg", - "oneOf": [ - { - "description": "Transfer is a base message to move a token to another account without triggering actions", - "type": "object", - "required": [ - "transfer_nft" - ], - "properties": { - "transfer_nft": { - "type": "object", - "required": [ - "recipient", - "token_id" - ], - "properties": { - "recipient": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted Approval", - "type": "object", - "required": [ - "revoke" - ], - "properties": { - "revoke": { - "type": "object", - "required": [ - "spender", - "token_id" - ], - "properties": { - "spender": { - "type": "string" - }, - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", - "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 - } - }, - "additionalProperties": false - }, - { - "description": "Remove previously granted ApproveAll permission", - "type": "object", - "required": [ - "revoke_all" - ], - "properties": { - "revoke_all": { - "type": "object", - "required": [ - "operator" - ], - "properties": { - "operator": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Burn an NFT the sender has access to", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "token_id" - ], - "properties": { - "token_id": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "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" - }, - "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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/execute_msg.json b/contracts/cw721-non-transferable/schema/execute_msg.json new file mode 100644 index 000000000..2bcf2fea7 --- /dev/null +++ b/contracts/cw721-non-transferable/schema/execute_msg.json @@ -0,0 +1,562 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "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 + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets address to send withdrawn fees to. Only owner can call this.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", + "type": "object", + "required": [ + "remove_withdraw_address" + ], + "properties": { + "remove_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", + "type": "object", + "required": [ + "withdraw_funds" + ], + "properties": { + "withdraw_funds": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "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" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "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 + } + ] + }, + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "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" + } + ] + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "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" + } + } +} diff --git a/contracts/cw721-non-transferable/schema/migrate_msg.json b/contracts/cw721-non-transferable/schema/migrate_msg.json new file mode 100644 index 000000000..f83fa9508 --- /dev/null +++ b/contracts/cw721-non-transferable/schema/migrate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "with_update" + ], + "properties": { + "with_update": { + "type": "object", + "properties": { + "creator": { + "type": [ + "string", + "null" + ] + }, + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/cw721-non-transferable/schema/nft_info_response.json b/contracts/cw721-non-transferable/schema/nft_info_response.json deleted file mode 100644 index 51b1f072c..000000000 --- a/contracts/cw721-non-transferable/schema/nft_info_response.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "NftInfoResponse", - "type": "object", - "properties": { - "extension": { - "description": "You can add any custom metadata here when you extend cw721-base", - "anyOf": [ - { - "$ref": "#/definitions/Empty" - }, - { - "type": "null" - } - ] - }, - "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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/num_tokens_response.json b/contracts/cw721-non-transferable/schema/num_tokens_response.json deleted file mode 100644 index aff5850c8..000000000 --- a/contracts/cw721-non-transferable/schema/num_tokens_response.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$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 -} diff --git a/contracts/cw721-non-transferable/schema/operators_response.json b/contracts/cw721-non-transferable/schema/operators_response.json deleted file mode 100644 index 533a096dd..000000000 --- a/contracts/cw721-non-transferable/schema/operators_response.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/owner_of_response.json b/contracts/cw721-non-transferable/schema/owner_of_response.json deleted file mode 100644 index abb9006d8..000000000 --- a/contracts/cw721-non-transferable/schema/owner_of_response.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "$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" - } - } -} diff --git a/contracts/cw721-non-transferable/schema/query_msg.json b/contracts/cw721-non-transferable/schema/query_msg.json index ffbb412b3..4cde7f9f8 100644 --- a/contracts/cw721-non-transferable/schema/query_msg.json +++ b/contracts/cw721-non-transferable/schema/query_msg.json @@ -291,6 +291,19 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_withdraw_address" + ], + "properties": { + "get_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] } diff --git a/contracts/cw721-non-transferable/schema/tokens_response.json b/contracts/cw721-non-transferable/schema/tokens_response.json deleted file mode 100644 index 4728d37e2..000000000 --- a/contracts/cw721-non-transferable/schema/tokens_response.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$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-non-transferable/src/lib.rs b/contracts/cw721-non-transferable/src/lib.rs index ed3fde750..ffdf96069 100644 --- a/contracts/cw721-non-transferable/src/lib.rs +++ b/contracts/cw721-non-transferable/src/lib.rs @@ -1,8 +1,9 @@ pub use crate::msg::{InstantiateMsg, QueryMsg}; use cosmwasm_std::Empty; +use cw721::state::DefaultOptionMetadataExtension; pub use cw721_base::{ entry::{execute as _execute, query as _query}, - ContractError, Cw721Contract, ExecuteMsg, Extension, InstantiateMsg as Cw721BaseInstantiateMsg, + Cw721Contract, }; pub mod msg; @@ -13,7 +14,8 @@ pub mod state; const CONTRACT_NAME: &str = "crates.io:cw721-non-transferable"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -pub type Cw721NonTransferableContract<'a> = Cw721Contract<'a, Extension, Empty, Empty, Empty>; +pub type Cw721NonTransferableContract<'a> = + Cw721Contract<'a, DefaultOptionMetadataExtension, Empty, Empty, Empty>; #[cfg(not(feature = "library"))] pub mod entry { @@ -24,6 +26,9 @@ pub mod entry { entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; + use cw721::error::Cw721ContractError; + use cw721::execute::Cw721Execute; + use cw721::msg::{Cw721ExecuteMsg, Cw721InstantiateMsg}; #[entry_point] pub fn instantiate( @@ -31,7 +36,7 @@ pub mod entry { env: Env, info: MessageInfo, msg: InstantiateMsg, - ) -> Result { + ) -> Result { let admin_addr: Option = msg .admin .as_deref() @@ -42,7 +47,7 @@ pub mod entry { CONFIG.save(deps.storage, &config)?; - let cw721_base_instantiate_msg = Cw721BaseInstantiateMsg { + let cw721_base_instantiate_msg = Cw721InstantiateMsg { name: msg.name, symbol: msg.symbol, minter: msg.minter, @@ -54,6 +59,8 @@ pub mod entry { env, info, cw721_base_instantiate_msg, + "contract_name", + "contract_version", )?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -68,28 +75,28 @@ pub mod entry { deps: DepsMut, env: Env, info: MessageInfo, - msg: ExecuteMsg, - ) -> Result { + msg: Cw721ExecuteMsg, + ) -> Result { let config = CONFIG.load(deps.storage)?; match config.admin { Some(admin) => { if admin == info.sender { _execute(deps, env, info, msg) } else { - Err(ContractError::Ownership( + Err(Cw721ContractError::Ownership( cw721_base::OwnershipError::NotOwner, )) } } None => match msg { - ExecuteMsg::Mint { + Cw721ExecuteMsg::Mint { token_id, owner, token_uri, extension, } => Cw721NonTransferableContract::default() .mint(deps, info, token_id, owner, token_uri, extension), - _ => Err(ContractError::Ownership( + _ => Err(Cw721ContractError::Ownership( cw721_base::OwnershipError::NotOwner, )), }, diff --git a/contracts/cw721-non-transferable/src/msg.rs b/contracts/cw721-non-transferable/src/msg.rs index 703a9a7d6..8c8af154c 100644 --- a/contracts/cw721-non-transferable/src/msg.rs +++ b/contracts/cw721-non-transferable/src/msg.rs @@ -1,6 +1,8 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::Empty; -use cw721_base::msg::QueryMsg as Cw721QueryMsg; +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::msg::{Cw721ExecuteMsg as ExecuteMsg, Cw721MigrateMsg as MigrateMsg, *}; +use cw721::state::DefaultOptionMetadataExtension; #[cw_serde] pub struct InstantiateMsg { @@ -14,6 +16,8 @@ pub struct InstantiateMsg { #[cw_serde] pub enum QueryMsg { Admin {}, + + // -- below copied from Cw721QueryMsg OwnerOf { token_id: String, include_expired: Option, @@ -35,6 +39,7 @@ pub enum QueryMsg { }, NumTokens {}, ContractInfo {}, + NftInfo { token_id: String, }, @@ -52,10 +57,12 @@ pub enum QueryMsg { limit: Option, }, Minter {}, + + GetWithdrawAddress {}, } -impl From for Cw721QueryMsg { - fn from(msg: QueryMsg) -> Cw721QueryMsg { +impl From for Cw721QueryMsg { + fn from(msg: QueryMsg) -> Cw721QueryMsg { match msg { QueryMsg::OwnerOf { token_id, @@ -87,7 +94,11 @@ impl From for Cw721QueryMsg { Cw721QueryMsg::AllTokens { start_after, limit } } QueryMsg::Minter {} => Cw721QueryMsg::Minter {}, - _ => unreachable!("cannot convert {:?} to Cw721QueryMsg", msg), + QueryMsg::GetWithdrawAddress {} => Cw721QueryMsg::GetWithdrawAddress {}, + QueryMsg::AllOperators { .. } => unreachable!("AllOperators is not supported!"), + QueryMsg::Approval { .. } => unreachable!("Approval is not supported!"), + QueryMsg::Approvals { .. } => unreachable!("Approvals is not supported!"), + QueryMsg::Admin { .. } => unreachable!("Approvals is not supported!"), } } } diff --git a/contracts/cw721-non-transferable/src/state.rs b/contracts/cw721-non-transferable/src/state.rs index 594bde6e1..13ef71c10 100644 --- a/contracts/cw721-non-transferable/src/state.rs +++ b/contracts/cw721-non-transferable/src/state.rs @@ -2,6 +2,9 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; use cw_storage_plus::Item; +// expose to all others using contract, so others dont need to import cw721 +pub use cw721::state::*; + #[cw_serde] pub struct Config { pub admin: Option, diff --git a/contracts/cw721-receiver-tester/schema/cw721-receiver-tester.json b/contracts/cw721-receiver-tester/schema/cw721-receiver-tester.json index 639e533cb..a26f45a43 100644 --- a/contracts/cw721-receiver-tester/schema/cw721-receiver-tester.json +++ b/contracts/cw721-receiver-tester/schema/cw721-receiver-tester.json @@ -1,6 +1,6 @@ { "contract_name": "cw721-receiver-tester", - "contract_version": "0.18.0", + "contract_version": "0.19.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/cw721-receiver-tester/src/msg.rs b/contracts/cw721-receiver-tester/src/msg.rs index 137c09046..f6b7cce50 100644 --- a/contracts/cw721-receiver-tester/src/msg.rs +++ b/contracts/cw721-receiver-tester/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cw721::Cw721ReceiveMsg; +use cw721::receiver::Cw721ReceiveMsg; #[cw_serde] pub struct InstantiateMsg {} diff --git a/packages/cw1155/Cargo.toml b/packages/cw1155/Cargo.toml index a31669731..103a3224a 100644 --- a/packages/cw1155/Cargo.toml +++ b/packages/cw1155/Cargo.toml @@ -2,16 +2,17 @@ name = "cw1155" authors = ["shab "] description = "Definition and types for the CosmWasm-1155 interface" -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } documentation = { workspace = true } [dependencies] cw2 = { workspace = true } cw721 = { workspace = true } +cw-storage-plus = { workspace = true } cw-utils = { workspace = true } cw-ownable = { workspace = true } cosmwasm-std = { workspace = true } diff --git a/packages/cw1155/examples/schema.rs b/packages/cw1155/examples/schema.rs index f29cfdbe1..915ade699 100644 --- a/packages/cw1155/examples/schema.rs +++ b/packages/cw1155/examples/schema.rs @@ -1,14 +1,12 @@ use cosmwasm_schema::write_api; use cosmwasm_std::Empty; - -use cw1155::{Cw1155ExecuteMsg, Cw1155InstantiateMsg, Cw1155QueryMsg}; - -type Extension = Option; +use cw1155::msg::{Cw1155ExecuteMsg, Cw1155InstantiateMsg, Cw1155QueryMsg}; +use cw721::state::DefaultOptionMetadataExtension; fn main() { write_api! { instantiate: Cw1155InstantiateMsg, - execute: Cw1155ExecuteMsg, - query: Cw1155QueryMsg, + execute: Cw1155ExecuteMsg, + query: Cw1155QueryMsg, } } diff --git a/packages/cw1155/src/event.rs b/packages/cw1155/src/event.rs index d5252a722..430808100 100644 --- a/packages/cw1155/src/event.rs +++ b/packages/cw1155/src/event.rs @@ -1,4 +1,4 @@ -use crate::TokenAmount; +use crate::msg::TokenAmount; use cosmwasm_std::{attr, Addr, Attribute, Event, Uint128}; /// Tracks token transfer actions diff --git a/packages/cw1155/src/execute.rs b/packages/cw1155/src/execute.rs new file mode 100644 index 000000000..f47a4a0b6 --- /dev/null +++ b/packages/cw1155/src/execute.rs @@ -0,0 +1,753 @@ +use cosmwasm_std::{ + Addr, BankMsg, Binary, CustomMsg, DepsMut, Empty, Env, Event, MessageInfo, Order, Response, + StdResult, Storage, SubMsg, Uint128, +}; +use cw2::set_contract_version; +use cw721::execute::{migrate_version, Cw721Execute}; +use cw721::state::CollectionInfo; +use cw_utils::Expiration; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::event::{ + ApproveAllEvent, ApproveEvent, BurnEvent, MintEvent, RevokeAllEvent, RevokeEvent, TransferEvent, +}; +use crate::msg::{Balance, Cw1155MintMsg, TokenAmount, TokenApproval}; +use crate::receiver::Cw1155BatchReceiveMsg; +use crate::state::TokenInfo; +use crate::{ + error::Cw1155ContractError, + msg::{Cw1155ExecuteMsg, Cw1155InstantiateMsg}, + receiver::Cw1155ReceiveMsg, + state::Cw1155Config, +}; + +pub trait Cw1155Execute< + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg +>: Cw721Execute< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg,TQueryExtensionMsg> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn instantiate( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw1155InstantiateMsg, + contract_name: &str, + contract_version: &str, + ) -> Result, Cw1155ContractError> { + set_contract_version(deps.storage, contract_name, contract_version)?; + let config = Cw1155Config::::default(); + let collection_info = CollectionInfo { + name: msg.name, + symbol: msg.symbol, + }; + config + .collection_info + .save(deps.storage, &collection_info)?; + + // store minter + let minter = match msg.minter { + Some(owner) => deps.api.addr_validate(&owner)?, + None => info.sender, + }; + self.initialize_minter(deps.storage, deps.api, Some(minter.as_ref()))?; + + // store total supply + config.supply.save(deps.storage, &Uint128::zero())?; + + Ok(Response::default().add_attribute("minter", minter)) + } + + fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw1155ExecuteMsg, + ) -> Result, Cw1155ContractError> { + let env = ExecuteEnv { deps, env, info }; + match msg { + // cw1155 + Cw1155ExecuteMsg::SendBatch { + from, + to, + batch, + msg, + } => self.send_batch(env, from, to, batch, msg), + Cw1155ExecuteMsg::MintBatch { recipient, msgs } => { + self.mint_batch(env, recipient, msgs) + } + Cw1155ExecuteMsg::BurnBatch { from, batch } => self.burn_batch(env, from, batch), + Cw1155ExecuteMsg::ApproveAll { operator, expires } => { + self.approve_all_cw1155(env, operator, expires) + } + Cw1155ExecuteMsg::RevokeAll { operator } => self.revoke_all_cw1155(env, operator), + + // cw721 + Cw1155ExecuteMsg::Send { + from, + to, + token_id, + amount, + msg, + } => self.send(env, from, to, token_id, amount, msg), + Cw1155ExecuteMsg::Mint { recipient, msg } => self.mint_cw1155(env, recipient, msg), + Cw1155ExecuteMsg::Burn { + from, + token_id, + amount, + } => self.burn(env, from, token_id, amount), + Cw1155ExecuteMsg::Approve { + spender, + token_id, + amount, + expires, + } => self.approve_token(env, spender, token_id, amount, expires), + Cw1155ExecuteMsg::Revoke { + spender, + token_id, + amount, + } => self.revoke_token(env, spender, token_id, amount), + Cw1155ExecuteMsg::UpdateOwnership(action) => Self::update_ownership(env, action), + + Cw1155ExecuteMsg::Extension { .. } => unimplemented!(), + } + } + + fn migrate( + &self, + deps: DepsMut, + _env: Env, + _msg: Empty, + contract_name: &str, + contract_version: &str, + ) -> Result { + let response = Response::::default(); + // migrate + let response = migrate_version(deps.storage, contract_name, contract_version, response)?; + Ok(response) + } + + fn mint_cw1155( + &self, + env: ExecuteEnv, + recipient: String, + msg: Cw1155MintMsg, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + let config = Cw1155Config::::default(); + + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let to = deps.api.addr_validate(&recipient)?; + + let mut rsp = Response::default(); + + let event = self.update_balances( + &mut deps, + &env, + None, + Some(to), + vec![TokenAmount { + token_id: msg.token_id.to_string(), + amount: msg.amount, + }], + )?; + rsp = rsp.add_event(event); + + // store token info if not exist (if it is the first mint) + if !config.tokens.has(deps.storage, &msg.token_id) { + let token_info = TokenInfo { + token_uri: msg.token_uri, + extension: msg.extension, + }; + config + .tokens + .save(deps.storage, &msg.token_id, &token_info)?; + } + + Ok(rsp) + } + + fn mint_batch( + &self, + env: ExecuteEnv, + recipient: String, + msgs: Vec>, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + let config = Cw1155Config::::default(); + + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let to = deps.api.addr_validate(&recipient)?; + + let batch = msgs + .iter() + .map(|msg| { + // store token info if not exist (if it is the first mint) + if !config.tokens.has(deps.storage, &msg.token_id) { + let token_info = TokenInfo { + token_uri: msg.token_uri.clone(), + extension: msg.extension.clone(), + }; + config + .tokens + .save(deps.storage, &msg.token_id, &token_info)?; + } + Ok(TokenAmount { + token_id: msg.token_id.to_string(), + amount: msg.amount, + }) + }) + .collect::>>()?; + + let mut rsp = Response::default(); + let event = self.update_balances(&mut deps, &env, None, Some(to), batch)?; + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn send( + &self, + env: ExecuteEnv, + from: Option, + to: String, + token_id: String, + amount: Uint128, + msg: Option, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + env, + info, + } = env; + + let from = if let Some(from) = from { + deps.api.addr_validate(&from)? + } else { + info.sender.clone() + }; + let to = deps.api.addr_validate(&to)?; + + let balance_update = + self.verify_approval(deps.storage, &env, &info, &from, &token_id, amount)?; + + let mut rsp = Response::::default(); + + let event = self.update_balances( + &mut deps, + &env, + Some(from.clone()), + Some(to.clone()), + vec![TokenAmount { + token_id: token_id.to_string(), + amount: balance_update.amount, + }], + )?; + rsp.events.push(event); + + if let Some(msg) = msg { + rsp.messages.push(SubMsg::new( + Cw1155ReceiveMsg { + operator: info.sender.to_string(), + from: Some(from.to_string()), + amount, + token_id, + msg, + } + .into_cosmos_msg(&info, to)?, + )); + } else { + // transfer funds along to recipient + if info.funds.len() > 0 { + let transfer_msg = BankMsg::Send { + to_address: to.to_string(), + amount: info.funds.to_vec(), + }; + rsp.messages.push(SubMsg::new(transfer_msg)); + } + } + + Ok(rsp) + } + + fn send_batch( + &self, + env: ExecuteEnv, + from: Option, + to: String, + batch: Vec, + msg: Option, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + env, + info, + } = env; + + let from = if let Some(from) = from { + deps.api.addr_validate(&from)? + } else { + info.sender.clone() + }; + let to = deps.api.addr_validate(&to)?; + + let batch = self.verify_approvals(deps.storage, &env, &info, &from, batch)?; + + let mut rsp = Response::::default(); + let event = self.update_balances( + &mut deps, + &env, + Some(from.clone()), + Some(to.clone()), + batch.to_vec(), + )?; + rsp.events.push(event); + + if let Some(msg) = msg { + rsp.messages.push(SubMsg::new( + Cw1155BatchReceiveMsg { + operator: info.sender.to_string(), + from: Some(from.to_string()), + batch, + msg, + } + .into_cosmos_msg(&info, to)?, + )); + } else { + // transfer funds along to recipient + if info.funds.len() > 0 { + let transfer_msg = BankMsg::Send { + to_address: to.to_string(), + amount: info.funds.to_vec(), + }; + rsp.messages.push(SubMsg::new(transfer_msg)); + } + } + + Ok(rsp) + } + + fn burn( + &self, + env: ExecuteEnv, + from: Option, + token_id: String, + amount: Uint128, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + + let from = if let Some(from) = from { + deps.api.addr_validate(&from)? + } else { + info.sender.clone() + }; + + // whoever can transfer these tokens can burn + let balance_update = + self.verify_approval(deps.storage, &env, &info, &from, &token_id, amount)?; + + let mut rsp = Response::default(); + + let event = self.update_balances( + &mut deps, + &env, + Some(from), + None, + vec![TokenAmount { + token_id, + amount: balance_update.amount, + }], + )?; + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn burn_batch( + &self, + env: ExecuteEnv, + from: Option, + batch: Vec, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + + let from = if let Some(from) = from { + deps.api.addr_validate(&from)? + } else { + info.sender.clone() + }; + + let batch = self.verify_approvals(deps.storage, &env, &info, &from, batch)?; + + let mut rsp = Response::default(); + let event = self.update_balances(&mut deps, &env, Some(from), None, batch)?; + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn approve_token( + &self, + env: ExecuteEnv, + operator: String, + token_id: String, + amount: Option, + expiration: Option, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { deps, info, env } = env; + let config = Cw1155Config::::default(); + + // reject expired data as invalid + let expiration = expiration.unwrap_or_default(); + if expiration.is_expired(&env.block) { + return Err(Cw1155ContractError::Expired {}); + } + + // get sender's token balance to get valid approval amount + let balance = config + .balances + .load(deps.storage, (info.sender.clone(), token_id.to_string()))?; + let approval_amount = amount.unwrap_or(Uint128::MAX).min(balance.amount); + + // store the approval + let operator = deps.api.addr_validate(&operator)?; + config.token_approves.save( + deps.storage, + (&token_id, &info.sender, &operator), + &TokenApproval { + amount: approval_amount, + expiration, + }, + )?; + + let mut rsp = Response::default(); + + let event = ApproveEvent::new(&info.sender, &operator, &token_id, approval_amount).into(); + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn approve_all_cw1155( + &self, + env: ExecuteEnv, + operator: String, + expires: Option, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { deps, info, env } = env; + let config = Cw1155Config::::default(); + + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&env.block) { + return Err(Cw1155ContractError::Expired {}); + } + + // set the operator for us + let operator = deps.api.addr_validate(&operator)?; + config + .approves + .save(deps.storage, (&info.sender, &operator), &expires)?; + + let mut rsp = Response::default(); + + let event = ApproveAllEvent::new(&info.sender, &operator).into(); + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn revoke_token( + &self, + env: ExecuteEnv, + operator: String, + token_id: String, + amount: Option, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { deps, info, .. } = env; + let config = Cw1155Config::::default(); + let operator = deps.api.addr_validate(&operator)?; + + // get prev approval amount to get valid revoke amount + let prev_approval = config + .token_approves + .load(deps.storage, (&token_id, &info.sender, &operator))?; + let revoke_amount = amount.unwrap_or(Uint128::MAX).min(prev_approval.amount); + + // remove or update approval + if revoke_amount == prev_approval.amount { + config + .token_approves + .remove(deps.storage, (&token_id, &info.sender, &operator)); + } else { + config.token_approves.update( + deps.storage, + (&token_id, &info.sender, &operator), + |prev| -> StdResult<_> { + let mut new_approval = prev.unwrap(); + new_approval.amount = new_approval.amount.checked_sub(revoke_amount)?; + Ok(new_approval) + }, + )?; + } + + let mut rsp = Response::default(); + + let event = RevokeEvent::new(&info.sender, &operator, &token_id, revoke_amount).into(); + rsp = rsp.add_event(event); + + Ok(rsp) + } + + fn revoke_all_cw1155( + &self, + env: ExecuteEnv, + operator: String, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { deps, info, .. } = env; + let config = Cw1155Config::::default(); + let operator = deps.api.addr_validate(&operator)?; + + config + .approves + .remove(deps.storage, (&info.sender, &operator)); + + let mut rsp = Response::default(); + + let event = RevokeAllEvent::new(&info.sender, &operator).into(); + rsp = rsp.add_event(event); + + Ok(rsp) + } + + /// When from is None: mint new tokens + /// When to is None: burn tokens + /// When both are Some: transfer tokens + /// + /// Make sure permissions are checked before calling this. + fn update_balances( + &self, + deps: &mut DepsMut, + env: &Env, + from: Option, + to: Option, + tokens: Vec, + ) -> Result { + let config = Cw1155Config::::default(); + if let Some(from) = &from { + for TokenAmount { token_id, amount } in tokens.iter() { + config.balances.update( + deps.storage, + (from.clone(), token_id.to_string()), + |balance: Option| -> StdResult<_> { + let mut new_balance = balance.unwrap(); + new_balance.amount = new_balance.amount.checked_sub(*amount)?; + Ok(new_balance) + }, + )?; + } + } + + if let Some(to) = &to { + for TokenAmount { token_id, amount } in tokens.iter() { + config.balances.update( + deps.storage, + (to.clone(), token_id.to_string()), + |balance: Option| -> StdResult<_> { + let mut new_balance: Balance = if let Some(balance) = balance { + balance + } else { + Balance { + owner: to.clone(), + amount: Uint128::zero(), + token_id: token_id.to_string(), + } + }; + + new_balance.amount = new_balance.amount.checked_add(*amount)?; + Ok(new_balance) + }, + )?; + } + } + + let event = if let Some(from) = &from { + for TokenAmount { token_id, amount } in &tokens { + // remove token approvals + for (operator, approval) in config + .token_approves + .prefix((token_id, from)) + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()? + { + if approval.is_expired(env) || approval.amount <= *amount { + config + .token_approves + .remove(deps.storage, (token_id, from, &operator)); + } else { + config.token_approves.update( + deps.storage, + (token_id, from, &operator), + |prev| -> StdResult<_> { + let mut new_approval = prev.unwrap(); + new_approval.amount = new_approval.amount.checked_sub(*amount)?; + Ok(new_approval) + }, + )?; + } + } + + // decrement tokens if burning + if to.is_none() { + config.decrement_tokens(deps.storage, token_id, amount)?; + } + } + + if let Some(to) = &to { + // transfer + TransferEvent::new(from, to, tokens).into() + } else { + // burn + BurnEvent::new(from, tokens).into() + } + } else if let Some(to) = &to { + // mint + for TokenAmount { token_id, amount } in &tokens { + config.increment_tokens(deps.storage, token_id, amount)?; + } + MintEvent::new(to, tokens).into() + } else { + panic!("Invalid transfer: from and to cannot both be None") + }; + + Ok(event) + } + + /// returns valid token amount if the sender can execute or is approved to execute + fn verify_approval( + &self, + storage: &dyn Storage, + env: &Env, + info: &MessageInfo, + owner: &Addr, + token_id: &str, + amount: Uint128, + ) -> Result { + let config = Cw1155Config::::default(); + let operator = &info.sender; + + let owner_balance = config + .balances + .load(storage, (owner.clone(), token_id.to_string()))?; + let mut balance_update = TokenAmount { + token_id: token_id.to_string(), + amount: owner_balance.amount.min(amount), + }; + + // owner or all operator can execute + if owner == operator || config.verify_all_approval(storage, env, owner, operator) { + return Ok(balance_update); + } + + // token operator can execute up to approved amount + if let Some(token_approval) = + self.get_active_token_approval(storage, env, owner, operator, token_id) + { + balance_update.amount = balance_update.amount.min(token_approval.amount); + return Ok(balance_update); + } + + Err(Cw1155ContractError::Unauthorized {}) + } + + /// returns valid token amounts if the sender can execute or is approved to execute on all provided tokens + fn verify_approvals( + &self, + storage: &dyn Storage, + env: &Env, + info: &MessageInfo, + owner: &Addr, + tokens: Vec, + ) -> Result, Cw1155ContractError> { + tokens + .iter() + .map(|TokenAmount { token_id, amount }| { + self.verify_approval(storage, env, info, owner, token_id, *amount) + }) + .collect() + } + + fn get_active_token_approval( + &self, + storage: &dyn Storage, + env: &Env, + owner: &Addr, + operator: &Addr, + token_id: &str, + ) -> Option { + let config = Cw1155Config::::default(); + match config + .token_approves + .load(storage, (token_id, owner, operator)) + { + Ok(approval) => { + if !approval.is_expired(env) { + Some(approval) + } else { + None + } + } + Err(_) => None, + } + } + + fn update_ownership( + env: ExecuteEnv, + action: cw_ownable::Action, + ) -> Result, Cw1155ContractError> { + let ExecuteEnv { deps, info, env } = env; + let ownership = + cw_ownable::update_ownership(deps.api, deps.storage, &env.block, &info.sender, action)?; + Ok(Response::new().add_attributes(ownership.into_attributes())) + } +} + +/// To mitigate clippy::too_many_arguments warning +pub struct ExecuteEnv<'a> { + deps: DepsMut<'a>, + env: Env, + info: MessageInfo, +} diff --git a/packages/cw1155/src/lib.rs b/packages/cw1155/src/lib.rs index 41a928d3b..18183c764 100644 --- a/packages/cw1155/src/lib.rs +++ b/packages/cw1155/src/lib.rs @@ -1,21 +1,9 @@ -mod error; -mod event; -mod msg; -mod query; -mod receiver; +pub mod error; +pub mod event; +pub mod execute; +pub mod msg; +pub mod query; +pub mod receiver; +pub mod state; pub use cw_utils::Expiration; - -pub use crate::receiver::{Cw1155BatchReceiveMsg, Cw1155ReceiveMsg}; - -pub use crate::msg::{ - Approval, Balance, Cw1155ExecuteMsg, Cw1155InstantiateMsg, Cw1155MintMsg, OwnerToken, - TokenAmount, TokenApproval, -}; -pub use crate::query::{ - AllTokenInfoResponse, ApprovedForAllResponse, BalanceResponse, BalancesResponse, - Cw1155QueryMsg, IsApprovedForAllResponse, NumTokensResponse, TokenInfoResponse, TokensResponse, -}; - -pub use crate::error::Cw1155ContractError; -pub use crate::event::*; diff --git a/packages/cw1155/src/msg.rs b/packages/cw1155/src/msg.rs index c6a0dd8b3..2630f035c 100644 --- a/packages/cw1155/src/msg.rs +++ b/packages/cw1155/src/msg.rs @@ -1,8 +1,9 @@ -use cosmwasm_schema::cw_serde; +use cosmwasm_schema::{cw_serde, QueryResponses}; use std::fmt::{Display, Formatter}; use cosmwasm_std::{Addr, Binary, Env, Uint128}; -use cw_ownable::cw_ownable_execute; +use cw721::Approval; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; use cw_utils::Expiration; #[cw_serde] @@ -24,7 +25,7 @@ pub struct Cw1155InstantiateMsg { /// use other control logic in any contract that inherits this. #[cw_ownable_execute] #[cw_serde] -pub enum Cw1155ExecuteMsg { +pub enum Cw1155ExecuteMsg { // cw1155 /// BatchSendFrom is a base message to move multiple types of tokens in batch, /// if `env.sender` is the owner or has sufficient pre-approval. @@ -40,7 +41,7 @@ pub enum Cw1155ExecuteMsg { /// Mint a batch of tokens, can only be called by the contract minter MintBatch { recipient: String, - msgs: Vec>, + msgs: Vec>, }, /// BatchBurn is a base message to burn multiple types of tokens in batch. BurnBatch { @@ -73,7 +74,7 @@ pub enum Cw1155ExecuteMsg { /// Mint a new NFT, can only be called by the contract minter Mint { recipient: String, - msg: Cw1155MintMsg, + msg: Cw1155MintMsg, }, /// Burn is a base message to burn tokens. Burn { @@ -100,7 +101,125 @@ pub enum Cw1155ExecuteMsg { }, /// Extension msg - Extension { msg: E }, + Extension { msg: TMetadataExtensionMsg }, +} + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum Cw1155QueryMsg { + // cw1155 + /// Returns the current balance of the given account, 0 if unset. + #[returns(BalanceResponse)] + BalanceOf(OwnerToken), + /// Returns the current balance of the given batch of accounts/tokens, 0 if unset. + #[returns(BalancesResponse)] + BalanceOfBatch(Vec), + /// Query approved status `owner` granted to `operator`. + #[returns(IsApprovedForAllResponse)] + IsApprovedForAll { owner: String, operator: String }, + /// Return approvals that a token owner has + #[returns(Vec)] + TokenApprovals { + owner: String, + token_id: String, + include_expired: Option, + }, + /// List all operators that can access all of the owner's tokens. + #[returns(ApprovedForAllResponse)] + ApprovalsForAll { + owner: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Returns all current balances of the given token id. Supports pagination + #[returns(BalancesResponse)] + AllBalances { + token_id: String, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + #[returns(NumTokensResponse)] + NumTokens { + token_id: Option, // optional token id to get supply of, otherwise total supply + }, + + // cw721 + /// With MetaData Extension. + /// Returns top-level metadata about the contract. + #[returns(cw721::state::CollectionInfo)] + ContractInfo {}, + /// Query Minter. + #[returns(cw721::msg::MinterResponse)] + Minter {}, + /// With MetaData Extension. + /// Query metadata of token + #[returns(TokenInfoResponse)] + TokenInfo { token_id: String }, + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + #[returns(cw721::msg::TokensResponse)] + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + #[returns(cw721::msg::TokensResponse)] + AllTokens { + start_after: Option, + limit: Option, + }, + + /// Extension query + #[returns(())] + Extension { + msg: TQueryExtensionMsg, + phantom: Option, // dummy field to infer type + }, +} + +#[cw_serde] +pub struct BalanceResponse { + pub balance: Uint128, +} + +#[cw_serde] +pub struct BalancesResponse { + pub balances: Vec, +} + +#[cw_serde] +pub struct NumTokensResponse { + pub count: Uint128, +} + +#[cw_serde] +pub struct ApprovedForAllResponse { + pub operators: Vec, +} + +#[cw_serde] +pub struct IsApprovedForAllResponse { + pub approved: bool, +} + +#[cw_serde] +pub struct AllTokenInfoResponse { + pub token_id: String, + pub info: TokenInfoResponse, +} + +#[cw_serde] +pub struct TokenInfoResponse { + /// Should be a url point to a json file + pub token_uri: Option, + /// You can add any custom metadata here when you extend cw1155-base + pub extension: T, } #[cw_serde] @@ -153,11 +272,3 @@ pub struct Balance { pub owner: Addr, pub amount: Uint128, } - -#[cw_serde] -pub struct Approval { - /// Account that can transfer/send the token - pub spender: String, - /// When the Approval expires (maybe Expiration::never) - pub expires: Expiration, -} diff --git a/packages/cw1155/src/query.rs b/packages/cw1155/src/query.rs index f13ff9db1..f0113c34b 100644 --- a/packages/cw1155/src/query.rs +++ b/packages/cw1155/src/query.rs @@ -1,129 +1,272 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use schemars::JsonSchema; +use cosmwasm_std::{to_json_binary, Addr, Binary, CustomMsg, Deps, Env, Order, StdResult, Uint128}; +use cw721::msg::TokensResponse; +use cw721::query::Cw721Query; +use cw721::Approval; +use cw_storage_plus::Bound; +use cw_utils::{maybe_addr, Expiration}; +use serde::de::DeserializeOwned; +use serde::Serialize; -use crate::{Approval, Balance, OwnerToken}; -use cosmwasm_std::Uint128; -use cw_ownable::cw_ownable_query; +use crate::msg::{ + ApprovedForAllResponse, Balance, BalanceResponse, BalancesResponse, Cw1155QueryMsg, + IsApprovedForAllResponse, OwnerToken, +}; +use crate::msg::{NumTokensResponse, TokenInfoResponse}; +use crate::state::Cw1155Config; -#[cw_ownable_query] -#[cw_serde] -#[derive(QueryResponses)] -pub enum Cw1155QueryMsg { - // cw1155 - /// Returns the current balance of the given account, 0 if unset. - #[returns(BalanceResponse)] - BalanceOf(OwnerToken), - /// Returns the current balance of the given batch of accounts/tokens, 0 if unset. - #[returns(BalancesResponse)] - BalanceOfBatch(Vec), - /// Query approved status `owner` granted to `operator`. - #[returns(IsApprovedForAllResponse)] - IsApprovedForAll { owner: String, operator: String }, - /// Return approvals that a token owner has - #[returns(Vec)] - TokenApprovals { - owner: String, - token_id: String, - include_expired: Option, - }, - /// List all operators that can access all of the owner's tokens. - #[returns(ApprovedForAllResponse)] - ApprovalsForAll { - owner: String, - /// unset or false will filter out expired approvals, you must set to true to see them - include_expired: Option, - start_after: Option, +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 1000; + +pub trait Cw1155Query< + // Metadata defined in NftInfo. + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg +>: Cw721Query where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn query( + &self, + deps: Deps, + env: Env, + msg: Cw1155QueryMsg, + ) -> StdResult { + match msg { + Cw1155QueryMsg::Minter {} => { + to_json_binary(&self.query_minter(deps.storage)?) + } + Cw1155QueryMsg::BalanceOf(OwnerToken { owner, token_id }) => { + let config = Cw1155Config::::default(); + let owner_addr = deps.api.addr_validate(&owner)?; + let balance = config + .balances + .may_load(deps.storage, (owner_addr.clone(), token_id.clone()))? + .unwrap_or(Balance { + owner: owner_addr, + token_id, + amount: Uint128::new(0), + }); + to_json_binary(&BalanceResponse { + balance: balance.amount, + }) + } + Cw1155QueryMsg::AllBalances { + token_id, + start_after, + limit, + } => to_json_binary(&self.query_all_balances(deps, token_id, start_after, limit)?), + Cw1155QueryMsg::BalanceOfBatch(batch) => { + let config = Cw1155Config::::default(); + let balances = batch + .into_iter() + .map(|OwnerToken { owner, token_id }| { + let owner = Addr::unchecked(owner); + config.balances + .load(deps.storage, (owner.clone(), token_id.to_string())) + .unwrap_or(Balance { + owner, + token_id, + amount: Uint128::zero(), + }) + }) + .collect::>(); + to_json_binary(&BalancesResponse { balances }) + } + Cw1155QueryMsg::TokenApprovals { + owner, + token_id, + include_expired, + } => { + let config = Cw1155Config::::default(); + let owner = deps.api.addr_validate(&owner)?; + let approvals = config + .token_approves + .prefix((&token_id, &owner)) + .range(deps.storage, None, None, Order::Ascending) + .filter_map(|approval| { + let (_, approval) = approval.unwrap(); + if include_expired.unwrap_or(false) || !approval.is_expired(&env) { + Some(approval) + } else { + None + } + }) + .collect::>(); + to_json_binary(&approvals) + } + Cw1155QueryMsg::ApprovalsForAll { + owner, + include_expired, + start_after, + limit, + } => { + let owner_addr = deps.api.addr_validate(&owner)?; + let start_addr = maybe_addr(deps.api, start_after)?; + to_json_binary(&self.query_all_approvals( + deps, + env, + owner_addr, + include_expired.unwrap_or(false), + start_addr, + limit, + )?) + } + Cw1155QueryMsg::IsApprovedForAll { owner, operator } => { + let config = Cw1155Config::::default(); + let owner_addr = deps.api.addr_validate(&owner)?; + let operator_addr = deps.api.addr_validate(&operator)?; + let approved = + config.verify_all_approval(deps.storage, &env, &owner_addr, &operator_addr); + to_json_binary(&IsApprovedForAllResponse { approved }) + } + Cw1155QueryMsg::TokenInfo { token_id } => { + let config = Cw1155Config::::default(); + let token_info = config.tokens.load(deps.storage, &token_id)?; + to_json_binary(&TokenInfoResponse:: { + token_uri: token_info.token_uri, + extension: token_info.extension, + }) + } + Cw1155QueryMsg::Tokens { + owner, + start_after, + limit, + } => { + let owner_addr = deps.api.addr_validate(&owner)?; + to_json_binary(&self.query_owner_tokens(deps, owner_addr, start_after, limit)?) + } + Cw1155QueryMsg::ContractInfo {} => { + to_json_binary(&self.query_collection_info(deps, env)?) + } + Cw1155QueryMsg::NumTokens { token_id } => { + let config = Cw1155Config::::default(); + let count = if let Some(token_id) = token_id { + config.token_count(deps.storage, &token_id)? + } else { + config.supply.load(deps.storage)? + }; + to_json_binary(&NumTokensResponse { count }) + } + Cw1155QueryMsg::AllTokens { start_after, limit } => { + to_json_binary(&self.query_all_tokens_cw1155(deps, start_after, limit)?) + } + Cw1155QueryMsg::Ownership {} => { + to_json_binary(&cw_ownable::get_ownership(deps.storage)?) + } + + Cw1155QueryMsg::Extension { msg: ext_msg, .. } => { + self.query_extension(deps, env, ext_msg) + } + } + } + + fn query_all_approvals( + &self, + deps: Deps, + env: Env, + owner: Addr, + include_expired: bool, + start_after: Option, limit: Option, - }, - /// Returns all current balances of the given token id. Supports pagination - #[returns(BalancesResponse)] - AllBalances { - token_id: String, + ) -> StdResult { + let config = Cw1155Config::::default(); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(Bound::exclusive); + + let operators = config + .approves + .prefix(&owner) + .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(build_approval) + .collect::>()?; + Ok(ApprovedForAllResponse { operators }) + } + + fn query_owner_tokens( + &self, + deps: Deps, + owner: Addr, start_after: Option, limit: Option, - }, - /// Total number of tokens issued - #[returns(cw721::NumTokensResponse)] - NumTokens { - token_id: Option, // optional token id to get supply of, otherwise total supply - }, + ) -> StdResult { + let config = Cw1155Config::::default(); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + + let tokens = config + .balances + .prefix(owner) + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>()?; + Ok(TokensResponse { tokens }) + } - // cw721 - /// With MetaData Extension. - /// Returns top-level metadata about the contract. - #[returns(cw721::ContractInfoResponse)] - ContractInfo {}, - /// Query Minter. - #[returns(cw721::MinterResponse)] - Minter {}, - /// With MetaData Extension. - /// Query metadata of token - #[returns(TokenInfoResponse)] - TokenInfo { token_id: String }, - /// With Enumerable extension. - /// Returns all tokens owned by the given address, [] if unset. - #[returns(TokensResponse)] - Tokens { - owner: String, + fn query_all_tokens_cw1155( + &self, + deps: Deps, start_after: Option, limit: Option, - }, - /// With Enumerable extension. - /// Requires pagination. Lists all token_ids controlled by the contract. - #[returns(TokensResponse)] - AllTokens { + ) -> StdResult { + let config = Cw1155Config::::default(); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + let tokens = config + .tokens + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>()?; + Ok(TokensResponse { tokens }) + } + + fn query_all_balances( + &self, + deps: Deps, + token_id: String, start_after: Option, limit: Option, - }, + ) -> StdResult { + let config = Cw1155Config::::default(); + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - /// Extension query - #[returns(())] - Extension { msg: Q }, -} + let start = if let Some(start_after) = start_after { + let start_key = (Addr::unchecked(start_after), token_id.clone()); + Some(Bound::exclusive::<(Addr, String)>(start_key)) + } else { + None + }; -#[cw_serde] -pub struct BalanceResponse { - pub balance: Uint128, -} - -#[cw_serde] -pub struct BalancesResponse { - pub balances: Vec, -} - -#[cw_serde] -pub struct NumTokensResponse { - pub count: Uint128, -} - -#[cw_serde] -pub struct ApprovedForAllResponse { - pub operators: Vec, -} - -#[cw_serde] -pub struct IsApprovedForAllResponse { - pub approved: bool, -} - -#[cw_serde] -pub struct AllTokenInfoResponse { - pub token_id: String, - pub info: TokenInfoResponse, -} + let balances: Vec = config + .balances + .idx + .token_id + .prefix(token_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, v) = item.unwrap(); + v + }) + .collect(); -#[cw_serde] -pub struct TokenInfoResponse { - /// Should be a url point to a json file - pub token_uri: Option, - /// You can add any custom metadata here when you extend cw1155-base - pub extension: T, + Ok(BalancesResponse { balances }) + } } -#[cw_serde] -pub struct TokensResponse { - /// Contains all token_ids in lexicographical ordering - /// If there are more than `limit`, use `start_from` in future queries - /// to achieve pagination. - pub tokens: Vec, +fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + item.map(|(addr, expires)| Approval { + spender: addr.into(), + expires, + }) } diff --git a/packages/cw1155/src/receiver.rs b/packages/cw1155/src/receiver.rs index b1efe4e33..090fb8e45 100644 --- a/packages/cw1155/src/receiver.rs +++ b/packages/cw1155/src/receiver.rs @@ -1,4 +1,4 @@ -use crate::TokenAmount; +use crate::msg::TokenAmount; use cosmwasm_schema::cw_serde; use cosmwasm_std::{to_json_binary, Binary, CosmosMsg, MessageInfo, StdResult, Uint128, WasmMsg}; use schemars::JsonSchema; diff --git a/packages/cw1155/src/state.rs b/packages/cw1155/src/state.rs new file mode 100644 index 000000000..8d4be7259 --- /dev/null +++ b/packages/cw1155/src/state.rs @@ -0,0 +1,190 @@ +use crate::msg::{Balance, TokenApproval}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, CustomMsg, Env, StdError, StdResult, Storage, Uint128}; +use cw721::state::CollectionInfo; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_utils::Expiration; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::marker::PhantomData; + +pub struct Cw1155Config< + 'a, + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + pub collection_info: Item<'a, CollectionInfo>, + pub supply: Item<'a, Uint128>, // total supply of all tokens + // key: token id + pub token_count: Map<'a, &'a str, Uint128>, // total supply of a specific token + // key: (owner, token id) + pub balances: IndexedMap<'a, (Addr, String), Balance, BalanceIndexes<'a>>, + // key: (owner, spender) + pub approves: Map<'a, (&'a Addr, &'a Addr), Expiration>, + // key: (token id, owner, spender) + pub token_approves: Map<'a, (&'a str, &'a Addr, &'a Addr), TokenApproval>, + // key: token id + pub tokens: Map<'a, &'a str, TokenInfo>, + + pub(crate) _custom_response: PhantomData, + pub(crate) _custom_execute: PhantomData, + pub(crate) _custom_query: PhantomData, +} + +impl Default + for Cw1155Config< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn default() -> Self { + Self::new( + "cw1155_contract_info", + "tokens", + "token_count", + "supply", + "balances", + "balances__token_id", + "approves", + "token_approves", + ) + } +} + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw1155Config< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + #[allow(clippy::too_many_arguments)] + fn new( + contract_info_key: &'a str, + tokens_key: &'a str, + token_count_key: &'a str, + supply_key: &'a str, + balances_key: &'a str, + balances_token_id_key: &'a str, + approves_key: &'a str, + token_approves_key: &'a str, + ) -> Self { + let balances_indexes = BalanceIndexes { + token_id: MultiIndex::new( + |_, b| b.token_id.to_string(), + balances_key, + balances_token_id_key, + ), + }; + Self { + collection_info: Item::new(contract_info_key), + tokens: Map::new(tokens_key), + token_count: Map::new(token_count_key), + supply: Item::new(supply_key), + balances: IndexedMap::new(balances_key, balances_indexes), + approves: Map::new(approves_key), + token_approves: Map::new(token_approves_key), + _custom_execute: PhantomData, + _custom_response: PhantomData, + _custom_query: PhantomData, + } + } + + pub fn token_count(&self, storage: &dyn Storage, token_id: &'a str) -> StdResult { + Ok(self + .token_count + .may_load(storage, token_id)? + .unwrap_or_default()) + } + + pub fn increment_tokens( + &self, + storage: &mut dyn Storage, + token_id: &'a str, + amount: &Uint128, + ) -> StdResult { + // increment token count + let val = self.token_count(storage, token_id)? + amount; + self.token_count.save(storage, token_id, &val)?; + + // increment total supply + self.supply.update(storage, |prev| { + Ok::(prev.checked_add(*amount)?) + })?; + + Ok(val) + } + + pub fn decrement_tokens( + &self, + storage: &mut dyn Storage, + token_id: &'a str, + amount: &Uint128, + ) -> StdResult { + // decrement token count + let val = self.token_count(storage, token_id)?.checked_sub(*amount)?; + self.token_count.save(storage, token_id, &val)?; + + // decrement total supply + self.supply.update(storage, |prev| { + Ok::(prev.checked_sub(*amount)?) + })?; + + Ok(val) + } + + pub fn verify_all_approval( + &self, + storage: &dyn Storage, + env: &Env, + owner: &Addr, + operator: &Addr, + ) -> bool { + match self.approves.load(storage, (owner, operator)) { + Ok(ex) => !ex.is_expired(&env.block), + Err(_) => false, + } + } +} + +#[cw_serde] +pub struct TokenInfo { + /// Metadata JSON Schema + pub token_uri: Option, + /// You can add any custom metadata here when you extend cw1155-base + pub extension: T, +} + +pub struct BalanceIndexes<'a> { + pub token_id: MultiIndex<'a, String, Balance, (Addr, String)>, +} + +impl<'a> IndexList for BalanceIndexes<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.token_id]; + Box::new(v.into_iter()) + } +} diff --git a/packages/cw721/Cargo.toml b/packages/cw721/Cargo.toml index 0f5967cff..e6f4775cc 100644 --- a/packages/cw721/Cargo.toml +++ b/packages/cw721/Cargo.toml @@ -15,6 +15,17 @@ documentation = { 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-016 = { workspace = true } schemars = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-base-016 = { workspace = true, features = ["library"] } +cw721-base-017 = { workspace = true, features = ["library"] } +cw721-base-018 = { workspace = true, features = ["library"] } diff --git a/packages/cw721/README.md b/packages/cw721/README.md index 883809f04..b9e0ee991 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -1,42 +1,58 @@ # CW721 Spec: Non Fungible Tokens -CW721 is a specification for non-fungible tokens based on CosmWasm. -The name and design is based on Ethereum's ERC721 standard, -with some enhancements. The types in here can be imported by -contracts that wish to implement this spec, or by contracts that call +CW721 is a specification for non-fungible tokens (NFTs) based on CosmWasm. +The name and design is based on Ethereum's [ERC721](https://eips.ethereum.org/EIPS/eip-721) standard, +with some enhancements. The types in here can be imported by +contracts that wish to implement this spec, or by contracts that call to any standard cw721 contract. The specification is split into multiple sections, a contract may only implement some of this functionality, but must implement the base. -## Base +## `cw721` package + +The CW721 package provides 2 traits with default implementations (aka `utilities`): + +- `Cw721Execute` with `Cw721ExecuteMsg` e.g. for minting, burning, + sending, approving, and transferring NFTs. It also allows updating + collection info, withdraw address, creator and minter ownership. +- `Cw721Query` with `Cw721QueryMsg` e.g. for NFTs, tokens, approvals, various kinds of ownerships (creator and minter). + +Default implementations are opionated and uses a `Cw721Config` store. Custom cw721 +contracts may re-implement each utlitiy to their own need. + +### `cw721-base` This handles ownership, transfers, and allowances. These must be supported -as is by all CW721 contracts. Note that all tokens must have an owner, +as is by all CW721 contracts. Note that all tokens must have an owner, as well as an ID. The ID is an arbitrary string, unique within the contract. +`cw721-base` contract is the base contract for handling NFT collections with +either offchain (stored in `token_uri`) or onchain (stored in `NftInfo`'s extension') +metadata. Contract itself is lightweight, since all logic is provided in `cw721` package. + ### Messages -`TransferNft{recipient, token_id}` - -This transfers ownership of the token to `recipient` account. This is -designed to send to an address controlled by a private key and *does not* +`TransferNft{recipient, token_id}` - +This transfers ownership of the token to `recipient` account. This is +designed to send to an address controlled by a private key and _does not_ trigger any actions on the recipient if it is a contract. -Requires `token_id` to point to a valid token, and `env.sender` to be -the owner of it, or have an allowance to transfer it. +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. -`SendNft{contract, token_id, msg}` - -This transfers ownership of the token to `contract` account. `contract` +`SendNft{contract, token_id, msg}` - +This transfers ownership of the token to `contract` account. `contract` must be an address controlled by a smart contract, which implements -the CW721Receiver interface. The `msg` will be passed to the recipient +the CW721Receiver interface. The `msg` will be passed to the recipient contract, along with the token_id. -Requires `token_id` to point to a valid token, and `env.sender` to be -the owner of it, or have an allowance to transfer it. +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. `Approve{spender, token_id, expires}` - Grants permission to `spender` to transfer or send the given token. This can only be performed when -`env.sender` is the owner of the given `token_id` or an `operator`. +`env.sender` is the owner of the given `token_id` or an `operator`. There can be multiple spender accounts per token, and they are cleared once the token is transferred or sent. @@ -72,14 +88,14 @@ expired owners in the results, otherwise, ignore them. operators that can access all of the owner's tokens. Return type is `OperatorsResponse`. If `include_expired` is set, show expired owners in the results, otherwise, ignore them. If `start_after` is set, then it returns the -first `limit` operators *after* the given one. +first `limit` operators _after_ the given one. `NumTokens{}` - Total number of tokens issued ### Receiver The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by -any contract that wishes to manage CW721 tokens. This is generally *not* +any contract that wishes to manage CW721 tokens. This is generally _not_ implemented by any CW721 contract. `ReceiveNft{sender, token_id, msg}` - This is designed to handle `SendNft` @@ -91,18 +107,18 @@ The `sender` is the original account requesting to move the token and `msg` is a `Binary` data that can be decoded into a contract-specific message. This can be empty if we have only one default action, or it may be a `ReceiveMsg` variant to clarify the intention. For example, -if I send to an exchange, I can specify the price I want to list the token +if I send to an exchange, I can specify the price I want to list the token for. - + ## Metadata ### Queries -`ContractInfo{}` - This returns top-level metadata about the contract. +`CollectionInfo{}` - This returns top-level metadata about the contract. Namely, `name` and `symbol`. `NftInfo{token_id}` - This returns metadata about one particular token. -The return value is based on *ERC721 Metadata JSON Schema*, but directly +The return value is based on _ERC721 Metadata JSON Schema_, but directly from the contract, not as a Uri. Only the image link is a Uri. `AllNftInfo{token_id}` - This returns the result of both `NftInfo` @@ -120,14 +136,72 @@ value (suggested 30). Contracts can define other `DefaultLimit` and `MaxLimit` values without violating the CW721 spec, and clients should not rely on any particular values. -If `start_after` is unset, the query returns the first results, ordered +If `start_after` is unset, the query returns the first results, ordered lexicographically by `token_id`. If `start_after` is set, then it returns the -first `limit` tokens *after* the given one. This allows straightforward +first `limit` tokens _after_ the given one. This allows straightforward pagination by taking the last result returned (a `token_id`) and using it -as the `start_after` value in a future query. +as the `start_after` value in a future query. `Tokens{owner, start_after, limit}` - List all token_ids that belong to a given owner. Return type is `TokensResponse{tokens: Vec}`. -`AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by +`AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by the contract. + +### NftInfo Extension - CW721 Metadata Onchain + +NFT creators may want to store their NFT metadata on-chain so other contracts are able to interact with it. +With CW721 in CosmWasm, we allow you to store any data on chain you wish, using a generic `extension: T`. + +In order to support on-chain metadata, and to demonstrate how to use the extension ability, we have created this simple contract. +There is no business logic here, but looking at `lib.rs` will show you how do define custom data that is included when minting and +available in all queries. + +In particular, here we define: + +```rust +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Trait { + pub display_type: Option, + pub trait_type: String, + pub value: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Metadata { + pub image: Option, + pub image_data: Option, + pub external_url: Option, + pub description: Option, + pub name: Option, + pub attributes: Option>, + pub background_color: Option, + pub animation_url: Option, + pub youtube_url: Option, +} + +pub type Extension = Option; +``` + +In particular, the fields defined conform to the properties supported in the [OpenSea Metadata Standard](https://docs.opensea.io/docs/metadata-standards). + + +This means when you query `NftInfo{name: "Enterprise"}`, you will get something like: + +```json +{ + "name": "Enterprise", + "token_uri": "https://starships.example.com/Starship/Enterprise.json", + "extension": { + "image": null, + "image_data": null, + "external_url": null, + "description": "Spaceship with Warp Drive", + "name": "Starship USS Enterprise", + "attributes": null, + "background_color": null, + "animation_url": null, + "youtube_url": null + } +} +``` diff --git a/packages/cw721/examples/schema.rs b/packages/cw721/examples/schema.rs index 2999afa75..fc8c4a765 100644 --- a/packages/cw721/examples/schema.rs +++ b/packages/cw721/examples/schema.rs @@ -2,27 +2,54 @@ use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; -use cosmwasm_std::Empty; +use cosmwasm_std::Empty; use cw721::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, Cw721ExecuteMsg, - Cw721QueryMsg, Cw721ReceiveMsg, NftInfoResponse, NumTokensResponse, OperatorResponse, - OperatorsResponse, OwnerOfResponse, TokensResponse, + msg::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, CollectionInfoMsg, + Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721MigrateMsg, Cw721QueryMsg, MinterResponse, + NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, + }, + receiver::Cw721ReceiveMsg, + state::{CollectionInfo, DefaultOptionMetadataExtension}, }; - -type Extension = Option; - fn main() { let mut out_dir = current_dir().unwrap(); out_dir.push("schema"); create_dir_all(&out_dir).unwrap(); remove_schemas(&out_dir).unwrap(); - export_schema(&schema_for!(Cw721ExecuteMsg), &out_dir); - export_schema(&schema_for!(Cw721QueryMsg), &out_dir); - export_schema(&schema_for!(Cw721ReceiveMsg), &out_dir); + // entry points - generate always with title for avoiding name suffixes like "..._empty_for_..." due to generics + export_schema_with_title( + &schema_for!(Cw721InstantiateMsg), + &out_dir, + "Cw721InstantiateMsg", + ); + export_schema_with_title( + &schema_for!(Cw721ExecuteMsg::), + &out_dir, + "Cw721ExecuteMsg", + ); export_schema_with_title( - &schema_for!(AllNftInfoResponse), + &schema_for!(Cw721QueryMsg), + &out_dir, + "Cw721QueryMsg", + ); + export_schema_with_title(&schema_for!(Cw721MigrateMsg), &out_dir, "Cw721MigrateMsg"); + + // messages + export_schema_with_title(&schema_for!(Cw721ReceiveMsg), &out_dir, "Cw721ReceiveMsg"); + export_schema(&schema_for!(CollectionInfoMsg), &out_dir); + + // responses + export_schema_with_title( + &schema_for!(NftInfoResponse), + &out_dir, + "NftInfoResponse", + ); + export_schema_with_title( + &schema_for!(AllNftInfoResponse), &out_dir, "AllNftInfoResponse", ); @@ -30,13 +57,9 @@ fn main() { export_schema(&schema_for!(ApprovalsResponse), &out_dir); export_schema(&schema_for!(OperatorResponse), &out_dir); export_schema(&schema_for!(OperatorsResponse), &out_dir); - export_schema(&schema_for!(ContractInfoResponse), &out_dir); + export_schema_with_title(&schema_for!(CollectionInfo), &out_dir, "CollectionInfo"); export_schema(&schema_for!(OwnerOfResponse), &out_dir); - export_schema_with_title( - &schema_for!(NftInfoResponse), - &out_dir, - "NftInfoResponse", - ); + export_schema(&schema_for!(MinterResponse), &out_dir); export_schema(&schema_for!(NumTokensResponse), &out_dir); export_schema(&schema_for!(TokensResponse), &out_dir); } diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json index d263321e2..8a03851da 100644 --- a/packages/cw721/schema/all_nft_info_response.json +++ b/packages/cw721/schema/all_nft_info_response.json @@ -19,13 +19,17 @@ "description": "Data on the token itself,", "allOf": [ { - "$ref": "#/definitions/NftInfoResponse_for_Nullable_Empty" + "$ref": "#/definitions/NftInfoResponse_for_Nullable_Metadata" } ] } }, "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" + }, "Approval": { "type": "object", "required": [ @@ -43,15 +47,15 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "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": [ @@ -99,14 +103,77 @@ } ] }, - "NftInfoResponse_for_Nullable_Empty": { + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "NftInfoResponse_for_Nullable_Metadata": { "type": "object", "properties": { "extension": { "description": "You can add any custom metadata here when you extend cw721-base", "anyOf": [ { - "$ref": "#/definitions/Empty" + "$ref": "#/definitions/Metadata" }, { "type": "null" @@ -152,6 +219,28 @@ } ] }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, "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" diff --git a/packages/cw721/schema/approval_response.json b/packages/cw721/schema/approval_response.json index b29eab59e..c72b71b2d 100644 --- a/packages/cw721/schema/approval_response.json +++ b/packages/cw721/schema/approval_response.json @@ -12,6 +12,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -29,7 +33,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false diff --git a/packages/cw721/schema/approvals_response.json b/packages/cw721/schema/approvals_response.json index 7cdac0015..01caf4d31 100644 --- a/packages/cw721/schema/approvals_response.json +++ b/packages/cw721/schema/approvals_response.json @@ -15,6 +15,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -32,7 +36,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false diff --git a/packages/cw721/schema/contract_info_response.json b/packages/cw721/schema/collection_info.json similarity index 88% rename from packages/cw721/schema/contract_info_response.json rename to packages/cw721/schema/collection_info.json index 4a805a825..ebb8e6e8e 100644 --- a/packages/cw721/schema/contract_info_response.json +++ b/packages/cw721/schema/collection_info.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ContractInfoResponse", + "title": "CollectionInfo", "type": "object", "required": [ "name", diff --git a/contracts/cw721-non-transferable/schema/contract_info_response.json b/packages/cw721/schema/collection_info_msg.json similarity index 88% rename from contracts/cw721-non-transferable/schema/contract_info_response.json rename to packages/cw721/schema/collection_info_msg.json index 4a805a825..060c8f6a4 100644 --- a/contracts/cw721-non-transferable/schema/contract_info_response.json +++ b/packages/cw721/schema/collection_info_msg.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ContractInfoResponse", + "title": "CollectionInfoMsg", "type": "object", "required": [ "name", diff --git a/packages/cw721/schema/cw721_execute_msg.json b/packages/cw721/schema/cw721_execute_msg.json index 228b5c249..77cf0910f 100644 --- a/packages/cw721/schema/cw721_execute_msg.json +++ b/packages/cw721/schema/cw721_execute_msg.json @@ -2,6 +2,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cw721ExecuteMsg", "oneOf": [ + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, { "description": "Transfer is a base message to move a token to another account without triggering actions", "type": "object", @@ -174,6 +186,52 @@ }, "additionalProperties": false }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "anyOf": [ + { + "$ref": "#/definitions/Metadata" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "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 + } + }, + "additionalProperties": false + }, { "description": "Burn an NFT the sender has access to", "type": "object", @@ -195,13 +253,163 @@ } }, "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets address to send withdrawn fees to. Only owner can call this.", + "type": "object", + "required": [ + "set_withdraw_address" + ], + "properties": { + "set_withdraw_address": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes the withdraw address, so fees are sent to the contract. Only owner can call this.", + "type": "object", + "required": [ + "remove_withdraw_address" + ], + "properties": { + "remove_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Withdraw from the contract to the given address. Anyone can call this, which is okay since withdraw address has been set by owner.", + "type": "object", + "required": [ + "withdraw_funds" + ], + "properties": { + "withdraw_funds": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "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" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "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": [ @@ -249,6 +457,69 @@ } ] }, + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "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": [ @@ -257,6 +528,32 @@ } ] }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 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 `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, "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" diff --git a/packages/cw721/schema/cw721_instantiate_msg.json b/packages/cw721/schema/cw721_instantiate_msg.json new file mode 100644 index 000000000..337caa3c9 --- /dev/null +++ b/packages/cw721/schema/cw721_instantiate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721InstantiateMsg", + "type": "object", + "required": [ + "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", + "null" + ] + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + }, + "withdraw_address": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false +} diff --git a/packages/cw721/schema/cw721_migrate_msg.json b/packages/cw721/schema/cw721_migrate_msg.json new file mode 100644 index 000000000..4bc17cd39 --- /dev/null +++ b/packages/cw721/schema/cw721_migrate_msg.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "with_update" + ], + "properties": { + "with_update": { + "type": "object", + "properties": { + "creator": { + "type": [ + "string", + "null" + ] + }, + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/packages/cw721/schema/cw721_query_msg.json b/packages/cw721/schema/cw721_query_msg.json index 8d7c18acc..0323ea65c 100644 --- a/packages/cw721/schema/cw721_query_msg.json +++ b/packages/cw721/schema/cw721_query_msg.json @@ -3,7 +3,7 @@ "title": "Cw721QueryMsg", "oneOf": [ { - "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse", + "description": "Return the owner of the given token, error if token does not exist", "type": "object", "required": [ "owner_of" @@ -32,7 +32,7 @@ "additionalProperties": false }, { - "description": "Return operator that can access all of the owner's tokens. Return type: `ApprovalResponse`", + "description": "Return operator that can access all of the owner's tokens.", "type": "object", "required": [ "approval" @@ -64,7 +64,7 @@ "additionalProperties": false }, { - "description": "Return approvals that a token has Return type: `ApprovalsResponse`", + "description": "Return approvals that a token has", "type": "object", "required": [ "approvals" @@ -92,7 +92,7 @@ "additionalProperties": false }, { - "description": "Return approval of a given operator for all tokens of an owner, error if not set Return type: `OperatorResponse`", + "description": "Return approval of a given operator for all tokens of an owner, error if not set", "type": "object", "required": [ "operator" @@ -124,7 +124,7 @@ "additionalProperties": false }, { - "description": "List all operators that can access all of the owner's tokens Return type: `OperatorsResponse`", + "description": "List all operators that can access all of the owner's tokens", "type": "object", "required": [ "all_operators" @@ -181,7 +181,6 @@ "additionalProperties": false }, { - "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`", "type": "object", "required": [ "contract_info" @@ -195,7 +194,20 @@ "additionalProperties": false }, { - "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", "type": "object", "required": [ "nft_info" @@ -217,7 +229,7 @@ "additionalProperties": false }, { - "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`", + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", "type": "object", "required": [ "all_nft_info" @@ -246,7 +258,7 @@ "additionalProperties": false }, { - "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", "type": "object", "required": [ "tokens" @@ -282,7 +294,7 @@ "additionalProperties": false }, { - "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", "type": "object", "required": [ "all_tokens" @@ -310,6 +322,61 @@ } }, "additionalProperties": false + }, + { + "description": "Return the minter", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_withdraw_address" + ], + "properties": { + "get_withdraw_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Do not use - dummy extension query, needed for inferring type parameter during compile", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false + } + }, + "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" } - ] + } } diff --git a/contracts/cw721-non-transferable/schema/minter_response.json b/packages/cw721/schema/minter_response.json similarity index 68% rename from contracts/cw721-non-transferable/schema/minter_response.json rename to packages/cw721/schema/minter_response.json index e79df37e8..3d450621a 100644 --- a/contracts/cw721-non-transferable/schema/minter_response.json +++ b/packages/cw721/schema/minter_response.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MinterResponse", - "description": "Shows who can mint these tokens", + "description": "Deprecated: use Cw721QueryMsg::GetMinterOwnership instead! Shows who can mint these tokens.", "type": "object", "properties": { "minter": { diff --git a/packages/cw721/schema/nft_info_response.json b/packages/cw721/schema/nft_info_response.json index 51b1f072c..0f0859230 100644 --- a/packages/cw721/schema/nft_info_response.json +++ b/packages/cw721/schema/nft_info_response.json @@ -7,7 +7,7 @@ "description": "You can add any custom metadata here when you extend cw721-base", "anyOf": [ { - "$ref": "#/definitions/Empty" + "$ref": "#/definitions/Metadata" }, { "type": "null" @@ -24,9 +24,90 @@ }, "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" + "Metadata": { + "type": "object", + "properties": { + "animation_url": { + "type": [ + "string", + "null" + ] + }, + "attributes": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Trait" + } + }, + "background_color": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "external_url": { + "type": [ + "string", + "null" + ] + }, + "image": { + "type": [ + "string", + "null" + ] + }, + "image_data": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "youtube_url": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "Trait": { + "type": "object", + "required": [ + "trait_type", + "value" + ], + "properties": { + "display_type": { + "type": [ + "string", + "null" + ] + }, + "trait_type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "additionalProperties": false } } } diff --git a/packages/cw721/schema/operator_response.json b/packages/cw721/schema/operator_response.json index 9e2dbd3d2..097e08213 100644 --- a/packages/cw721/schema/operator_response.json +++ b/packages/cw721/schema/operator_response.json @@ -12,6 +12,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -29,7 +33,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false diff --git a/packages/cw721/schema/operators_response.json b/packages/cw721/schema/operators_response.json index 533a096dd..426cd48cc 100644 --- a/packages/cw721/schema/operators_response.json +++ b/packages/cw721/schema/operators_response.json @@ -15,6 +15,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -32,7 +36,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json index abb9006d8..36b85a171 100644 --- a/packages/cw721/schema/owner_of_response.json +++ b/packages/cw721/schema/owner_of_response.json @@ -21,6 +21,10 @@ }, "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" + }, "Approval": { "type": "object", "required": [ @@ -38,7 +42,11 @@ }, "spender": { "description": "Account that can transfer/send the token", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] } }, "additionalProperties": false diff --git a/packages/cw721/src/error.rs b/packages/cw721/src/error.rs new file mode 100644 index 000000000..99e274b3f --- /dev/null +++ b/packages/cw721/src/error.rs @@ -0,0 +1,27 @@ +use cosmwasm_std::StdError; +use cw_ownable::OwnershipError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum Cw721ContractError { + #[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 }, + + #[error("No withdraw address set")] + NoWithdrawAddress {}, +} diff --git a/packages/cw721/src/execute.rs b/packages/cw721/src/execute.rs new file mode 100644 index 000000000..73c68fd4e --- /dev/null +++ b/packages/cw721/src/execute.rs @@ -0,0 +1,698 @@ +use cosmwasm_std::{ + Addr, Api, BankMsg, Binary, Coin, CustomMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, + StdResult, Storage, +}; +use cw_ownable::{none_or, Action, Ownership, OwnershipError, OwnershipStore}; +use cw_storage_plus::Item; +use cw_utils::Expiration; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::{ + error::Cw721ContractError, + msg::{Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721MigrateMsg}, + receiver::Cw721ReceiveMsg, + state::{CollectionInfo, Cw721Config, DefaultOptionMetadataExtension, NftInfo, MINTER}, + Approval, +}; + +pub trait Cw721Execute< + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn instantiate( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: Cw721InstantiateMsg, + contract_name: &str, + contract_version: &str, + ) -> Result, Cw721ContractError> { + cw2::set_contract_version(deps.storage, contract_name, contract_version)?; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + let collection_info = CollectionInfo { + name: msg.name, + symbol: msg.symbol, + }; + config + .collection_info + .save(deps.storage, &collection_info)?; + + let minter = match msg.minter { + Some(owner) => deps.api.addr_validate(&owner)?, + None => info.sender, + }; + self.initialize_minter(deps.storage, deps.api, Some(minter.as_ref()))?; + + if let Some(withdraw_address) = msg.withdraw_address { + self.set_withdraw_address(deps, &minter, withdraw_address)?; + } + + Ok(Response::default().add_attribute("minter", minter)) + } + + fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721ExecuteMsg, + ) -> Result, Cw721ContractError> { + match msg { + Cw721ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + } => self.mint(deps, info, token_id, owner, token_uri, extension), + Cw721ExecuteMsg::Approve { + spender, + token_id, + expires, + } => self.approve(deps, env, info, spender, token_id, expires), + Cw721ExecuteMsg::Revoke { spender, token_id } => { + self.revoke(deps, env, info, spender, token_id) + } + Cw721ExecuteMsg::ApproveAll { operator, expires } => { + self.approve_all(deps, env, info, operator, expires) + } + Cw721ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator), + Cw721ExecuteMsg::TransferNft { + recipient, + token_id, + } => self.transfer_nft(deps, env, info, recipient, token_id), + Cw721ExecuteMsg::SendNft { + contract, + token_id, + msg, + } => self.send_nft(deps, env, info, contract, token_id, msg), + Cw721ExecuteMsg::Burn { token_id } => self.burn_nft(deps, env, info, token_id), + Cw721ExecuteMsg::UpdateOwnership(action) => { + self.update_minter_ownership(deps, env, info, action) + } + Cw721ExecuteMsg::Extension { msg } => { + self.update_metadata_extension(deps, env, info, msg) + } + Cw721ExecuteMsg::SetWithdrawAddress { address } => { + self.set_withdraw_address(deps, &info.sender, address) + } + Cw721ExecuteMsg::RemoveWithdrawAddress {} => { + self.remove_withdraw_address(deps.storage, &info.sender) + } + Cw721ExecuteMsg::WithdrawFunds { amount } => self.withdraw_funds(deps.storage, &amount), + } + } + + fn migrate( + &self, + deps: DepsMut, + env: Env, + msg: Cw721MigrateMsg, + contract_name: &str, + contract_version: &str, + ) -> Result { + let response = Response::::default(); + // first migrate legacy data ... + let response = + migrate_legacy_minter_and_creator(deps.storage, deps.api, &env, &msg, response)?; + let response = migrate_legacy_collection_info(deps.storage, &env, &msg, response)?; + // ... then migrate + let response = migrate_version(deps.storage, contract_name, contract_version, response)?; + // ... and update creator and minter AFTER legacy migration + let response = migrate_minter(deps.storage, deps.api, &env, &msg, response)?; + Ok(response) + } + + // ------- ERC721-based functions ------- + fn transfer_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: String, + token_id: String, + ) -> Result, Cw721ContractError> { + _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, Cw721ContractError> { + // Transfer token + _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, Cw721ContractError> { + _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, Cw721ContractError> { + _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, Cw721ContractError> { + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&env.block) { + return Err(Cw721ContractError::Expired {}); + } + + // set the operator for us + let operator_addr = deps.api.addr_validate(&operator)?; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + config + .operators + // stores info.sender as key (=granter, NFT owner) and operator as value (operator only(!) has control over NFTs of granter) + // check is done in `check_can_send()` + .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, Cw721ContractError> { + let operator_addr = deps.api.addr_validate(&operator)?; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + config + .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_nft( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + ) -> Result, Cw721ContractError> { + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + let token = config.nft_info.load(deps.storage, &token_id)?; + check_can_send(deps.as_ref(), &env, &info, &token)?; + + config.nft_info.remove(deps.storage, &token_id)?; + config.decrement_tokens(deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id)) + } + + // ------- opionated cw721 functions ------- + fn initialize_minter( + &self, + storage: &mut dyn Storage, + api: &dyn Api, + minter: Option<&str>, + ) -> StdResult> { + MINTER.initialize_owner(storage, api, minter) + } + + fn mint( + &self, + deps: DepsMut, + info: MessageInfo, + token_id: String, + owner: String, + token_uri: Option, + extension: TMetadataExtension, + ) -> Result, Cw721ContractError> { + MINTER.assert_owner(deps.storage, &info.sender)?; + + // create the token + let token = NftInfo { + owner: deps.api.addr_validate(&owner)?, + approvals: vec![], + token_uri, + extension, + }; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + config + .nft_info + .update(deps.storage, &token_id, |old| match old { + Some(_) => Err(Cw721ContractError::Claimed {}), + None => Ok(token), + })?; + + config.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)) + } + + fn update_minter_ownership( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + action: Action, + ) -> Result, Cw721ContractError> { + let ownership = + MINTER.update_ownership(deps.api, deps.storage, &env.block, &info.sender, action)?; + Ok(Response::new() + .add_attribute("update_minter_ownership", info.sender) + .add_attributes(ownership.into_attributes())) + } + + /// Allows creator to update onchain metadata. For now this is a no-op. + fn update_metadata_extension( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + _msg: TMetadataExtensionMsg, + ) -> Result, Cw721ContractError> { + cw_ownable::assert_owner(deps.storage, &info.sender)?; + Ok(Response::new().add_attribute("action", "update_metadata_extension")) + } + + fn set_withdraw_address( + &self, + deps: DepsMut, + sender: &Addr, + address: String, + ) -> Result, Cw721ContractError> { + cw_ownable::assert_owner(deps.storage, sender)?; + deps.api.addr_validate(&address)?; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + config.withdraw_address.save(deps.storage, &address)?; + Ok(Response::new() + .add_attribute("action", "set_withdraw_address") + .add_attribute("address", address)) + } + + fn remove_withdraw_address( + &self, + storage: &mut dyn Storage, + sender: &Addr, + ) -> Result, Cw721ContractError> { + cw_ownable::assert_owner(storage, sender)?; + let config = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default(); + let address = config.withdraw_address.may_load(storage)?; + match address { + Some(address) => { + config.withdraw_address.remove(storage); + Ok(Response::new() + .add_attribute("action", "remove_withdraw_address") + .add_attribute("address", address)) + } + None => Err(Cw721ContractError::NoWithdrawAddress {}), + } + } + + fn withdraw_funds( + &self, + storage: &mut dyn Storage, + amount: &Coin, + ) -> Result, Cw721ContractError> { + let withdraw_address = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .withdraw_address + .may_load(storage)?; + match withdraw_address { + Some(address) => { + let msg = BankMsg::Send { + to_address: address, + amount: vec![amount.clone()], + }; + Ok(Response::new() + .add_message(msg) + .add_attribute("action", "withdraw_funds") + .add_attribute("amount", amount.amount.to_string()) + .add_attribute("denom", amount.denom.to_string())) + } + None => Err(Cw721ContractError::NoWithdrawAddress {}), + } + } +} + +// ------- helper cw721 functions ------- +fn _transfer_nft( + deps: DepsMut, + env: &Env, + info: &MessageInfo, + recipient: &str, + token_id: &str, +) -> Result, Cw721ContractError> +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + let config = Cw721Config::::default(); + let mut token = config.nft_info.load(deps.storage, token_id)?; + // ensure we have permissions + 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![]; + config.nft_info.save(deps.storage, token_id, &token)?; + Ok(token) +} + +#[allow(clippy::too_many_arguments)] +fn _update_approvals( + 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, Cw721ContractError> +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + let config = Cw721Config::::default(); + let mut token = config.nft_info.load(deps.storage, token_id)?; + // ensure we have permissions + 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(Cw721ContractError::Expired {}); + } + let approval = Approval { + spender: spender_addr, + expires, + }; + token.approvals.push(approval); + } + + config.nft_info.save(deps.storage, token_id, &token)?; + + Ok(token) +} + +/// returns true if the sender can execute approve or reject on the contract +pub fn check_can_approve( + deps: Deps, + env: &Env, + info: &MessageInfo, + token: &NftInfo, +) -> Result<(), Cw721ContractError> +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + // owner can approve + if token.owner == info.sender { + return Ok(()); + } + // operator can approve + let config = Cw721Config::::default(); + let op = config + .operators + .may_load(deps.storage, (&token.owner, &info.sender))?; + match op { + Some(ex) => { + if ex.is_expired(&env.block) { + Err(Cw721ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(Cw721ContractError::Ownership(OwnershipError::NotOwner)), + } +} + +/// returns true iff the sender can transfer ownership of the token +pub fn check_can_send( + deps: Deps, + env: &Env, + info: &MessageInfo, + token: &NftInfo, +) -> Result<(), Cw721ContractError> { + // 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 config = Cw721Config::::default(); + let op = config + .operators + // has token owner approved/gave grant to sender for full control over owner's NFTs? + .may_load(deps.storage, (&token.owner, &info.sender))?; + + match op { + Some(ex) => { + if ex.is_expired(&env.block) { + Err(Cw721ContractError::Ownership(OwnershipError::NotOwner)) + } else { + Ok(()) + } + } + None => Err(Cw721ContractError::Ownership(OwnershipError::NotOwner)), + } +} + +// ------- migrate ------- +pub fn migrate_version( + storage: &mut dyn Storage, + contradct_name: &str, + contract_version: &str, + response: Response, +) -> StdResult { + let response = response + .add_attribute("from_version", cw2::get_contract_version(storage)?.version) + .add_attribute("to_version", contract_version); + + // update contract version + cw2::set_contract_version(storage, contradct_name, contract_version)?; + Ok(response) +} + +pub fn migrate_minter( + storage: &mut dyn Storage, + api: &dyn Api, + _env: &Env, + msg: &Cw721MigrateMsg, + response: Response, +) -> StdResult { + match msg { + Cw721MigrateMsg::WithUpdate { minter, .. } => { + if let Some(minter) = minter { + MINTER.initialize_owner(storage, api, Some(minter.as_str()))?; + return Ok(response.add_attribute("creator", minter)); + } + } + } + Ok(response) +} + +/// Migrates only in case ownership is not present +/// !!! Important note here: !!! +/// - creator owns the contract and can update collection info +/// - minter can mint new tokens +/// +/// Before v0.19.0 there were confusing naming conventions: +/// - v0.17.0: minter was replaced by cw_ownable, as a result minter is owner +/// - v0.16.0 and below: minter was stored in dedicated `minter` store (so NOT using cw_ownable at all) +pub fn migrate_legacy_minter_and_creator( + storage: &mut dyn Storage, + api: &dyn Api, + _env: &Env, + _msg: &Cw721MigrateMsg, + response: Response, +) -> Result { + let minter = MINTER.item.may_load(storage)?; + // no migration in case minter is already set + if minter.is_some() { + return Ok(response); + } + // in v0.17/18 cw_ownable::OWNERSHIP was used for minter, now it is used for creator + let ownership_previously_used_as_minter = OwnershipStore::new("collection_minter") + .item + .may_load(storage)?; + let creator_and_minter = match ownership_previously_used_as_minter { + // v0.18 migration + Some(ownership) => { + // owner is used for both: creator and minter + // since it is already set for creator, we only need to migrate minter + let owner = ownership.owner.map(|a| a.to_string()); + MINTER.initialize_owner(storage, api, owner.as_deref())?; + owner + } + // v0.17 and older migration + None => { + let legacy_minter_store: Item = Item::new("minter"); + let legacy_minter = legacy_minter_store.load(storage)?; + MINTER.initialize_owner(storage, api, Some(legacy_minter.as_str()))?; + Some(legacy_minter.to_string()) + } + }; + Ok(response.add_attribute("creator_and_minter", none_or(creator_and_minter.as_ref()))) +} + +/// Migrates only in case collection_info is not present +pub fn migrate_legacy_collection_info( + storage: &mut dyn Storage, + _env: &Env, + _msg: &Cw721MigrateMsg, + response: Response, +) -> Result { + let contract = Cw721Config::::default(); + match contract.collection_info.may_load(storage)? { + Some(_) => Ok(response), + None => { + // contract info is legacy collection info + let legacy_collection_info_store: Item = + Item::new("nft_info"); + let legacy_collection_info = legacy_collection_info_store.load(storage)?; + let collection_info = CollectionInfo { + name: legacy_collection_info.name.clone(), + symbol: legacy_collection_info.symbol.clone(), + }; + contract.collection_info.save(storage, &collection_info)?; + Ok(response + .add_attribute("migrated collection name", legacy_collection_info.name) + .add_attribute("migrated collection symbol", legacy_collection_info.symbol)) + } + } +} diff --git a/contracts/cw721-base/src/helpers.rs b/packages/cw721/src/helpers.rs similarity index 72% rename from contracts/cw721-base/src/helpers.rs rename to packages/cw721/src/helpers.rs index ab28a52d6..67af25325 100644 --- a/contracts/cw721-base/src/helpers.rs +++ b/packages/cw721/src/helpers.rs @@ -1,32 +1,42 @@ use std::marker::PhantomData; +use crate::msg::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, NftInfoResponse, NumTokensResponse, + OperatorsResponse, OwnerOfResponse, TokensResponse, +}; +use crate::msg::{Cw721ExecuteMsg, Cw721QueryMsg}; +use crate::state::CollectionInfo; +use crate::Approval; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ to_json_binary, Addr, CosmosMsg, CustomMsg, QuerierWrapper, StdResult, WasmMsg, WasmQuery, }; -use cw721::{ - AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, - NftInfoResponse, NumTokensResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, -}; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::{ExecuteMsg, QueryMsg}; - #[cw_serde] -pub struct Cw721Contract( +pub struct Cw721Contract( pub Addr, - pub PhantomData, - pub PhantomData, + pub PhantomData, + pub PhantomData, + pub PhantomData, ); #[allow(dead_code)] -impl Cw721Contract { +impl + Cw721Contract +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ pub fn addr(&self) -> Addr { self.0.clone() } - pub fn call(&self, msg: ExecuteMsg) -> StdResult { + pub fn call( + &self, + msg: Cw721ExecuteMsg, + ) -> StdResult { let msg = to_json_binary(&msg)?; Ok(WasmMsg::Execute { contract_addr: self.addr().into(), @@ -39,7 +49,7 @@ impl Cw721Contract { pub fn query( &self, querier: &QuerierWrapper, - req: QueryMsg, + req: Cw721QueryMsg, ) -> StdResult { let query = WasmQuery::Smart { contract_addr: self.addr().into(), @@ -57,7 +67,7 @@ impl Cw721Contract { token_id: T, include_expired: bool, ) -> StdResult { - let req = QueryMsg::OwnerOf { + let req = Cw721QueryMsg::OwnerOf { token_id: token_id.into(), include_expired: Some(include_expired), }; @@ -71,7 +81,7 @@ impl Cw721Contract { spender: T, include_expired: Option, ) -> StdResult { - let req = QueryMsg::Approval { + let req = Cw721QueryMsg::Approval { token_id: token_id.into(), spender: spender.into(), include_expired, @@ -86,7 +96,7 @@ impl Cw721Contract { token_id: T, include_expired: Option, ) -> StdResult { - let req = QueryMsg::Approvals { + let req = Cw721QueryMsg::Approvals { token_id: token_id.into(), include_expired, }; @@ -102,7 +112,7 @@ impl Cw721Contract { start_after: Option, limit: Option, ) -> StdResult> { - let req = QueryMsg::AllOperators { + let req = Cw721QueryMsg::AllOperators { owner: owner.into(), include_expired: Some(include_expired), start_after, @@ -113,14 +123,14 @@ impl Cw721Contract { } pub fn num_tokens(&self, querier: &QuerierWrapper) -> StdResult { - let req = QueryMsg::NumTokens {}; + let req = Cw721QueryMsg::NumTokens {}; let res: NumTokensResponse = self.query(querier, req)?; Ok(res.count) } /// With metadata extension - pub fn contract_info(&self, querier: &QuerierWrapper) -> StdResult { - let req = QueryMsg::ContractInfo {}; + pub fn collection_info(&self, querier: &QuerierWrapper) -> StdResult { + let req = Cw721QueryMsg::ContractInfo {}; self.query(querier, req) } @@ -130,7 +140,7 @@ impl Cw721Contract { querier: &QuerierWrapper, token_id: T, ) -> StdResult> { - let req = QueryMsg::NftInfo { + let req = Cw721QueryMsg::NftInfo { token_id: token_id.into(), }; self.query(querier, req) @@ -143,7 +153,7 @@ impl Cw721Contract { token_id: T, include_expired: bool, ) -> StdResult> { - let req = QueryMsg::AllNftInfo { + let req = Cw721QueryMsg::AllNftInfo { token_id: token_id.into(), include_expired: Some(include_expired), }; @@ -158,7 +168,7 @@ impl Cw721Contract { start_after: Option, limit: Option, ) -> StdResult { - let req = QueryMsg::Tokens { + let req = Cw721QueryMsg::Tokens { owner: owner.into(), start_after, limit, @@ -173,15 +183,10 @@ impl Cw721Contract { start_after: Option, limit: Option, ) -> StdResult { - let req = QueryMsg::AllTokens { start_after, limit }; + let req = Cw721QueryMsg::AllTokens { start_after, limit }; self.query(querier, req) } - /// returns true if the contract supports the metadata extension - pub fn has_metadata(&self, querier: &QuerierWrapper) -> bool { - self.contract_info(querier).is_ok() - } - /// returns true if the contract supports the enumerable extension pub fn has_enumerable(&self, querier: &QuerierWrapper) -> bool { self.tokens(querier, self.addr(), None, Some(1)).is_ok() diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 0784c6272..dc1f7a396 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,15 +1,13 @@ -mod msg; -mod query; -mod receiver; -mod traits; +pub mod error; +pub mod execute; +pub mod helpers; +pub mod msg; +pub mod query; +pub mod receiver; +pub mod state; pub use cw_utils::Expiration; +pub use state::Approval; -pub use crate::msg::Cw721ExecuteMsg; -pub use crate::query::{ - AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, - Cw721QueryMsg, MinterResponse, NftInfoResponse, NumTokensResponse, OperatorResponse, - OperatorsResponse, OwnerOfResponse, TokensResponse, -}; -pub use crate::receiver::Cw721ReceiveMsg; -pub use crate::traits::{Cw721, Cw721Execute, Cw721Query}; +#[cfg(test)] +pub mod testing; diff --git a/packages/cw721/src/msg.rs b/packages/cw721/src/msg.rs index 87ce6cea3..2f975db52 100644 --- a/packages/cw721/src/msg.rs +++ b/packages/cw721/src/msg.rs @@ -1,11 +1,20 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::Binary; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, Coin}; +use cw_ownable::{Action, Ownership}; use cw_utils::Expiration; +use crate::state::CollectionInfo; +use crate::Approval; + #[cw_serde] -pub enum Cw721ExecuteMsg { +pub enum Cw721ExecuteMsg { + UpdateOwnership(Action), + /// Transfer is a base message to move a token to another account without triggering actions - TransferNft { recipient: String, token_id: String }, + TransferNft { + recipient: String, + token_id: String, + }, /// Send is a base message to transfer a token to a contract and trigger an action /// on the receiving contract. SendNft { @@ -21,7 +30,10 @@ pub enum Cw721ExecuteMsg { expires: Option, }, /// Remove previously granted Approval - Revoke { spender: String, token_id: String }, + Revoke { + spender: String, + token_id: String, + }, /// Allows operator to transfer / send any token from the owner's account. /// If expiration is set, then this allowance has a time/height limit ApproveAll { @@ -29,7 +41,236 @@ pub enum Cw721ExecuteMsg { expires: Option, }, /// Remove previously granted ApproveAll permission - RevokeAll { operator: String }, + RevokeAll { + operator: String, + }, + + /// Mint a new NFT, can only be called by the contract minter + Mint { + /// Unique ID of the NFT + token_id: String, + /// The owner of the newly minter NFT + owner: String, + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + token_uri: Option, + /// Any custom extension used by this contract + extension: TMetadataExtension, + }, + /// Burn an NFT the sender has access to - Burn { token_id: String }, + Burn { + token_id: String, + }, + + /// Extension msg + Extension { + msg: TMetadataExtensionMsg, + }, + + /// Sets address to send withdrawn fees to. Only owner can call this. + SetWithdrawAddress { + address: String, + }, + /// Removes the withdraw address, so fees are sent to the contract. Only owner can call this. + RemoveWithdrawAddress {}, + /// Withdraw from the contract to the given address. Anyone can call this, + /// which is okay since withdraw address has been set by owner. + WithdrawFunds { + amount: Coin, + }, +} + +#[cw_serde] +pub struct Cw721InstantiateMsg { + /// 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: Option, + + pub withdraw_address: Option, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum Cw721QueryMsg { + /// Return the owner of the given token, error if token does not exist + #[returns(OwnerOfResponse)] + OwnerOf { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + /// Return operator that can access all of the owner's tokens. + #[returns(ApprovalResponse)] + Approval { + token_id: String, + spender: String, + include_expired: Option, + }, + /// Return approvals that a token has + #[returns(ApprovalsResponse)] + Approvals { + token_id: String, + include_expired: Option, + }, + /// Return approval of a given operator for all tokens of an owner, error if not set + #[returns(OperatorResponse)] + Operator { + owner: String, + operator: String, + include_expired: Option, + }, + /// List all operators that can access all of the owner's tokens + #[returns(OperatorsResponse)] + AllOperators { + owner: String, + /// unset or false will filter out expired items, you must set to true to see them + include_expired: Option, + start_after: Option, + limit: Option, + }, + /// Total number of tokens issued + #[returns(NumTokensResponse)] + NumTokens {}, + + #[returns(CollectionInfo)] + ContractInfo {}, + + #[returns(Ownership)] + Ownership {}, + + /// With MetaData Extension. + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract + #[returns(NftInfoResponse)] + NftInfo { token_id: String }, + /// With MetaData Extension. + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients + #[returns(AllNftInfoResponse)] + AllNftInfo { + token_id: String, + /// unset or false will filter out expired approvals, you must set to true to see them + include_expired: Option, + }, + + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + #[returns(TokensResponse)] + Tokens { + owner: String, + start_after: Option, + limit: Option, + }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + #[returns(TokensResponse)] + AllTokens { + start_after: Option, + limit: Option, + }, + + /// Return the minter + #[returns(MinterResponse)] + Minter {}, + + #[returns(Option)] + GetWithdrawAddress {}, + + // -- below queries, Extension and GetCollectionInfoExtension, are just dummies, since type annotations are required for + // -- TMetadataExtension and TCollectionInfoExtension, Error: + // -- "type annotations needed: cannot infer type for type parameter `TMetadataExtension` declared on the enum `Cw721QueryMsg`" + /// Do not use - dummy extension query, needed for inferring type parameter during compile + #[returns(())] + Extension { + msg: TQueryExtensionMsg, + phantom: Option, // dummy field to infer type + }, +} + +#[cw_serde] +pub enum Cw721MigrateMsg { + WithUpdate { + minter: Option, + creator: Option, + }, +} + +#[cw_serde] +pub struct CollectionInfoMsg { + pub name: String, + pub symbol: String, +} + +#[cw_serde] +pub struct OwnerOfResponse { + /// Owner of the token + pub owner: String, + /// If set this address is approved to transfer/send the token as well + pub approvals: Vec, +} + +#[cw_serde] +pub struct ApprovalResponse { + pub approval: Approval, +} + +#[cw_serde] +pub struct ApprovalsResponse { + pub approvals: Vec, +} + +#[cw_serde] +pub struct OperatorResponse { + pub approval: Approval, +} + +#[cw_serde] +pub struct OperatorsResponse { + pub operators: Vec, +} + +#[cw_serde] +pub struct NumTokensResponse { + pub count: u64, +} + +#[cw_serde] +pub struct NftInfoResponse { + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// You can add any custom metadata here when you extend cw721-base + pub extension: TMetadataExtension, +} + +#[cw_serde] +pub struct AllNftInfoResponse { + /// Who can transfer the token + pub access: OwnerOfResponse, + /// Data on the token itself, + pub info: NftInfoResponse, +} + +#[cw_serde] +pub struct TokensResponse { + /// Contains all token_ids in lexicographical ordering + /// If there are more than `limit`, use `start_after` in future queries + /// to achieve pagination. + pub tokens: Vec, +} + +/// Deprecated: use Cw721QueryMsg::GetMinterOwnership instead! +/// Shows who can mint these tokens. +#[cw_serde] +pub struct MinterResponse { + pub minter: Option, } diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs index 0c921217d..24013d9f8 100644 --- a/packages/cw721/src/query.rs +++ b/packages/cw721/src/query.rs @@ -1,155 +1,484 @@ -use cosmwasm_schema::cw_serde; -use cw_utils::Expiration; - -#[cw_serde] -pub enum Cw721QueryMsg { - /// Return the owner of the given token, error if token does not exist - /// Return type: OwnerOfResponse - OwnerOf { - token_id: String, - /// unset or false will filter out expired approvals, you must set to true to see them - include_expired: Option, +use cosmwasm_std::{ + to_json_binary, Addr, Binary, BlockInfo, CustomMsg, Deps, Env, Order, StdError, StdResult, + Storage, +}; +use cw_ownable::Ownership; +use cw_storage_plus::Bound; +use cw_utils::{maybe_addr, Expiration}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::{ + msg::{ + AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, Cw721QueryMsg, MinterResponse, + NftInfoResponse, NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, + TokensResponse, }, - /// Return operator that can access all of the owner's tokens. - /// Return type: `ApprovalResponse` - Approval { + state::{Approval, CollectionInfo, Cw721Config, NftInfo, MINTER}, +}; + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 1000; + +pub trait Cw721Query< + // Metadata defined in NftInfo. + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn query( + &self, + deps: Deps, + env: Env, + msg: Cw721QueryMsg, + ) -> StdResult { + match msg { + Cw721QueryMsg::Minter {} => to_json_binary(&self.query_minter(deps.storage)?), + Cw721QueryMsg::ContractInfo {} => { + to_json_binary(&self.query_collection_info(deps, env)?) + } + Cw721QueryMsg::NftInfo { token_id } => { + to_json_binary(&self.query_nft_info(deps, env, token_id)?) + } + Cw721QueryMsg::OwnerOf { + token_id, + include_expired, + } => to_json_binary(&self.query_owner_of( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?), + Cw721QueryMsg::AllNftInfo { + token_id, + include_expired, + } => to_json_binary(&self.query_all_nft_info( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?), + Cw721QueryMsg::Operator { + owner, + operator, + include_expired, + } => to_json_binary(&self.query_operator( + deps, + env, + owner, + operator, + include_expired.unwrap_or(false), + )?), + Cw721QueryMsg::AllOperators { + owner, + include_expired, + start_after, + limit, + } => to_json_binary(&self.query_operators( + deps, + env, + owner, + include_expired.unwrap_or(false), + start_after, + limit, + )?), + Cw721QueryMsg::NumTokens {} => to_json_binary(&self.query_num_tokens(deps, env)?), + Cw721QueryMsg::Tokens { + owner, + start_after, + limit, + } => to_json_binary(&self.query_tokens(deps, env, owner, start_after, limit)?), + Cw721QueryMsg::AllTokens { start_after, limit } => { + to_json_binary(&self.query_all_tokens(deps, env, start_after, limit)?) + } + Cw721QueryMsg::Approval { + token_id, + spender, + include_expired, + } => to_json_binary(&self.query_approval( + deps, + env, + token_id, + spender, + include_expired.unwrap_or(false), + )?), + Cw721QueryMsg::Approvals { + token_id, + include_expired, + } => to_json_binary(&self.query_approvals( + deps, + env, + token_id, + include_expired.unwrap_or(false), + )?), + Cw721QueryMsg::Ownership {} => { + to_json_binary(&self.query_minter_ownership(deps.storage)?) + } + Cw721QueryMsg::Extension { msg, .. } => { + to_json_binary(&self.query_extension(deps, env, msg)?) + } + Cw721QueryMsg::GetWithdrawAddress {} => { + to_json_binary(&self.query_withdraw_address(deps)?) + } + } + } + + fn query_minter(&self, storage: &dyn Storage) -> StdResult { + let minter = MINTER + .get_ownership(storage)? + .owner + .map(|a| a.into_string()); + + Ok(MinterResponse { minter }) + } + + fn query_minter_ownership(&self, storage: &dyn Storage) -> StdResult> { + MINTER.get_ownership(storage) + } + + fn query_collection_info(&self, deps: Deps, _env: Env) -> StdResult { + Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .collection_info + .load(deps.storage) + } + + fn query_num_tokens(&self, deps: Deps, _env: Env) -> StdResult { + let count = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .token_count(deps.storage)?; + Ok(NumTokensResponse { count }) + } + + fn query_nft_info( + &self, + deps: Deps, + _env: Env, token_id: String, - spender: String, - include_expired: Option, - }, - /// Return approvals that a token has - /// Return type: `ApprovalsResponse` - Approvals { + ) -> StdResult> { + let info = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .load(deps.storage, &token_id)?; + Ok(NftInfoResponse { + token_uri: info.token_uri, + extension: info.extension, + }) + } + + fn query_owner_of( + &self, + deps: Deps, + env: Env, token_id: String, - include_expired: Option, - }, - /// Return approval of a given operator for all tokens of an owner, error if not set - /// Return type: `OperatorResponse` - Operator { + include_expired_approval: bool, + ) -> StdResult { + let nft_info = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .load(deps.storage, &token_id)?; + Ok(OwnerOfResponse { + owner: nft_info.owner.to_string(), + approvals: humanize_approvals(&env.block, &nft_info, include_expired_approval), + }) + } + + /// operator returns the approval status of an operator for a given owner if exists + fn query_operator( + &self, + deps: Deps, + env: Env, owner: String, operator: String, - include_expired: Option, - }, - /// List all operators that can access all of the owner's tokens - /// Return type: `OperatorsResponse` - AllOperators { + include_expired_approval: bool, + ) -> StdResult { + let owner_addr = deps.api.addr_validate(&owner)?; + let operator_addr = deps.api.addr_validate(&operator)?; + + let info = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .operators + .may_load(deps.storage, (&owner_addr, &operator_addr))?; + + if let Some(expires) = info { + if !include_expired_approval && expires.is_expired(&env.block) { + return Err(StdError::not_found("Approval not found")); + } + + return Ok(OperatorResponse { + approval: Approval { + spender: operator_addr, + expires, + }, + }); + } + + Err(StdError::not_found("Approval not found")) + } + + /// operators returns all operators owner given access to + fn query_operators( + &self, + deps: Deps, + env: Env, owner: String, - /// unset or false will filter out expired items, you must set to true to see them - include_expired: Option, + include_expired_approval: bool, start_after: Option, limit: Option, - }, - /// Total number of tokens issued - NumTokens {}, - - /// With MetaData Extension. - /// Returns top-level metadata about the contract: `ContractInfoResponse` - ContractInfo {}, - /// With MetaData Extension. - /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* - /// but directly from the contract: `NftInfoResponse` - NftInfo { token_id: String }, - /// With MetaData Extension. - /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization - /// for clients: `AllNftInfo` - AllNftInfo { + ) -> 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> = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .operators + .prefix(&owner_addr) + .range(deps.storage, start, None, Order::Ascending) + .filter(|r| { + include_expired_approval || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block) + }) + .take(limit) + .map(parse_approval) + .collect(); + Ok(OperatorsResponse { operators: res? }) + } + + fn query_approval( + &self, + deps: Deps, + env: Env, token_id: String, - /// unset or false will filter out expired approvals, you must set to true to see them - include_expired: Option, - }, + spender: String, + include_expired_approval: bool, + ) -> StdResult { + let token = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .load(deps.storage, &token_id)?; + + // token owner has absolute approval + if token.owner == spender { + let approval = Approval { + spender: token.owner, + expires: Expiration::Never {}, + }; + return Ok(ApprovalResponse { approval }); + } + + let filtered: Vec<_> = token + .approvals + .into_iter() + .filter(|t| t.spender == spender) + .filter(|t| include_expired_approval || !t.is_expired(&env.block)) + .map(|a| Approval { + spender: a.spender, + 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 + fn query_approvals( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired_approval: bool, + ) -> StdResult { + let token = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .load(deps.storage, &token_id)?; + let approvals: Vec<_> = token + .approvals + .into_iter() + .filter(|t| include_expired_approval || !t.is_expired(&env.block)) + .map(|a| Approval { + spender: a.spender, + expires: a.expires, + }) + .collect(); - /// With Enumerable extension. - /// Returns all tokens owned by the given address, [] if unset. - /// Return type: TokensResponse. - Tokens { + Ok(ApprovalsResponse { approvals }) + } + + fn query_tokens( + &self, + deps: Deps, + _env: Env, owner: String, start_after: Option, limit: Option, - }, - /// With Enumerable extension. - /// Requires pagination. Lists all token_ids controlled by the contract. - /// Return type: TokensResponse. - AllTokens { - 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())); -#[cw_serde] -pub struct OwnerOfResponse { - /// Owner of the token - pub owner: String, - /// If set this address is approved to transfer/send the token as well - pub approvals: Vec, -} + let owner_addr = deps.api.addr_validate(&owner)?; + let tokens: Vec = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .idx + .owner + .prefix(owner_addr) + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; -#[cw_serde] -pub struct Approval { - /// Account that can transfer/send the token - pub spender: String, - /// When the Approval expires (maybe Expiration::never) - pub expires: Expiration, -} + Ok(TokensResponse { tokens }) + } -#[cw_serde] -pub struct ApprovalResponse { - pub approval: Approval, -} + fn query_all_tokens( + &self, + deps: Deps, + _env: Env, + 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())); -#[cw_serde] -pub struct ApprovalsResponse { - pub approvals: Vec, -} + let tokens: StdResult> = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(k, _)| k)) + .collect(); -#[cw_serde] -pub struct OperatorResponse { - pub approval: Approval, -} + Ok(TokensResponse { tokens: tokens? }) + } -#[cw_serde] -pub struct OperatorsResponse { - pub operators: Vec, -} - -#[cw_serde] -pub struct NumTokensResponse { - pub count: u64, -} + fn query_all_nft_info( + &self, + deps: Deps, + env: Env, + token_id: String, + include_expired_approval: bool, + ) -> StdResult> { + let nft_info = Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .nft_info + .load(deps.storage, &token_id)?; + Ok(AllNftInfoResponse { + access: OwnerOfResponse { + owner: nft_info.owner.to_string(), + approvals: humanize_approvals(&env.block, &nft_info, include_expired_approval), + }, + info: NftInfoResponse { + token_uri: nft_info.token_uri, + extension: nft_info.extension, + }, + }) + } -#[cw_serde] -pub struct ContractInfoResponse { - pub name: String, - pub symbol: String, -} + /// No-op returning empty Binary + fn query_extension( + &self, + _deps: Deps, + _env: Env, + _msg: TQueryExtensionMsg, + ) -> StdResult { + Ok(Binary::default()) + } -#[cw_serde] -pub struct NftInfoResponse { - /// Universal resource identifier for this NFT - /// Should point to a JSON file that conforms to the ERC721 - /// Metadata JSON Schema - pub token_uri: Option, - /// You can add any custom metadata here when you extend cw721-base - pub extension: T, + fn query_withdraw_address(&self, deps: Deps) -> StdResult> { + Cw721Config::< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >::default() + .withdraw_address + .may_load(deps.storage) + } } -#[cw_serde] -pub struct AllNftInfoResponse { - /// Who can transfer the token - pub access: OwnerOfResponse, - /// Data on the token itself, - pub info: NftInfoResponse, +pub fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + item.map(|(spender, expires)| Approval { spender, expires }) } -#[cw_serde] -pub struct TokensResponse { - /// Contains all token_ids in lexicographical ordering - /// If there are more than `limit`, use `start_after` in future queries - /// to achieve pagination. - pub tokens: Vec, +pub fn humanize_approvals( + block: &BlockInfo, + nft_info: &NftInfo, + include_expired_approval: bool, +) -> Vec +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + nft_info + .approvals + .iter() + .filter(|apr| include_expired_approval || !apr.is_expired(block)) + .map(humanize_approval) + .collect() } -/// Shows who can mint these tokens -#[cw_serde] -pub struct MinterResponse { - pub minter: Option, +pub fn humanize_approval(approval: &Approval) -> Approval { + Approval { + spender: approval.spender.clone(), + expires: approval.expires, + } } diff --git a/packages/cw721/src/receiver.rs b/packages/cw721/src/receiver.rs index e67097927..6d050b29c 100644 --- a/packages/cw721/src/receiver.rs +++ b/packages/cw721/src/receiver.rs @@ -19,9 +19,12 @@ impl Cw721ReceiveMsg { } /// creates a cosmos_msg sending this struct to the named contract - pub fn into_cosmos_msg, C>(self, contract_addr: T) -> StdResult> + pub fn into_cosmos_msg, TCustomResponseMessage>( + self, + contract_addr: TAddress, + ) -> StdResult> where - C: Clone + std::fmt::Debug + PartialEq + JsonSchema, + TCustomResponseMessage: Clone + std::fmt::Debug + PartialEq + JsonSchema, { let msg = self.into_json_binary()?; let execute = WasmMsg::Execute { diff --git a/packages/cw721/src/state.rs b/packages/cw721/src/state.rs new file mode 100644 index 000000000..a7babdbbb --- /dev/null +++ b/packages/cw721/src/state.rs @@ -0,0 +1,205 @@ +use std::marker::PhantomData; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, BlockInfo, CustomMsg, StdResult, Storage}; +use cw_ownable::{OwnershipStore, OWNERSHIP_KEY}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; +use cw_utils::Expiration; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// - minter is stored in the contract storage using cw_ownable::OwnershipStore (same as for OWNERSHIP but with different key) +pub const MINTER: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY); + +/// Default CollectionInfoExtension with RoyaltyInfo +pub type DefaultOptionMetadataExtension = Option; + +pub struct Cw721Config< + 'a, + // Metadata defined in NftInfo (used for mint). + TMetadataExtension, + // Defines for `CosmosMsg::Custom` in response. Barely used, so `Empty` can be used. + TCustomResponseMessage, + // Message passed for updating metadata. + TMetadataExtensionMsg, + // Extension query message. + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + /// Note: replaces deprecated/legacy key "nft_info"! + pub collection_info: Item<'a, CollectionInfo>, + pub token_count: Item<'a, u64>, + /// Stored as (granter, operator) giving operator full control over granter's account. + /// NOTE: granter is the owner, so operator has only control for NFTs owned by granter! + pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>, + pub nft_info: + IndexedMap<'a, &'a str, NftInfo, TokenIndexes<'a, TMetadataExtension>>, + pub withdraw_address: Item<'a, String>, + + pub(crate) _custom_response: PhantomData, + pub(crate) _custom_execute: PhantomData, + pub(crate) _custom_query: PhantomData, +} + +impl Default + for Cw721Config< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn default() -> Self { + Self::new( + "collection_info", // Note: replaces deprecated/legacy key "nft_info" + "num_tokens", + "operators", + "tokens", + "tokens__owner", + "withdraw_address", + ) + } +} + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Config< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn new( + collection_info_key: &'a str, + token_count_key: &'a str, + operator_key: &'a str, + nft_info_key: &'a str, + nft_info_owner_key: &'a str, + withdraw_address_key: &'a str, + ) -> Self { + let indexes = TokenIndexes { + owner: MultiIndex::new(token_owner_idx, nft_info_key, nft_info_owner_key), + }; + Self { + collection_info: Item::new(collection_info_key), + token_count: Item::new(token_count_key), + operators: Map::new(operator_key), + nft_info: IndexedMap::new(nft_info_key, indexes), + withdraw_address: Item::new(withdraw_address_key), + _custom_response: PhantomData, + _custom_execute: PhantomData, + _custom_query: PhantomData, + } + } + + pub fn token_count(&self, storage: &dyn Storage) -> StdResult { + Ok(self.token_count.may_load(storage)?.unwrap_or_default()) + } + + pub fn increment_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count(storage)? + 1; + self.token_count.save(storage, &val)?; + Ok(val) + } + + pub fn decrement_tokens(&self, storage: &mut dyn Storage) -> StdResult { + let val = self.token_count(storage)? - 1; + self.token_count.save(storage, &val)?; + Ok(val) + } +} + +pub fn token_owner_idx(_pk: &[u8], d: &NftInfo) -> Addr { + d.owner.clone() +} + +#[cw_serde] +pub struct NftInfo { + /// 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, + + /// You can add any custom metadata here when you extend cw721-base + pub extension: TMetadataExtension, +} + +#[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) + } +} + +pub struct TokenIndexes<'a, TMetadataExtension> +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + pub owner: MultiIndex<'a, Addr, NftInfo, String>, +} + +impl<'a, TMetadataExtension> IndexList> + for TokenIndexes<'a, TMetadataExtension> +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, +{ + fn get_indexes( + &'_ self, + ) -> Box>> + '_> { + let v: Vec<&dyn Index>> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} + +#[cw_serde] +pub struct CollectionInfo { + pub name: String, + pub symbol: String, +} + +// see: https://docs.opensea.io/docs/metadata-standards +#[cw_serde] +#[derive(Default)] +pub struct Metadata { + pub image: Option, + pub image_data: Option, + pub external_url: Option, + pub description: Option, + pub name: Option, + pub attributes: Option>, + pub background_color: Option, + pub animation_url: Option, + pub youtube_url: Option, +} + +#[cw_serde] +pub struct Trait { + pub display_type: Option, + pub trait_type: String, + pub value: String, +} diff --git a/packages/cw721/src/testing/contract.rs b/packages/cw721/src/testing/contract.rs new file mode 100644 index 000000000..1158f42fd --- /dev/null +++ b/packages/cw721/src/testing/contract.rs @@ -0,0 +1,93 @@ +use cosmwasm_std::CustomMsg; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::execute::Cw721Execute; +use crate::query::Cw721Query; +use crate::state::Cw721Config; + +pub struct Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, +> where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + pub config: Cw721Config< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + >, +} + +impl Default + for Cw721Contract< + 'static, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ + fn default() -> Self { + Self { + config: Cw721Config::default(), + } + } +} + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Execute< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ +} + +impl<'a, TMetadataExtension, TCustomResponseMessage, TMetadataExtensionMsg, TQueryExtensionMsg> + Cw721Query< + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > + for Cw721Contract< + 'a, + TMetadataExtension, + TCustomResponseMessage, + TMetadataExtensionMsg, + TQueryExtensionMsg, + > +where + TMetadataExtension: Serialize + DeserializeOwned + Clone, + TCustomResponseMessage: CustomMsg, + TMetadataExtensionMsg: CustomMsg, + TQueryExtensionMsg: Serialize + DeserializeOwned + Clone, +{ +} diff --git a/contracts/cw721-base/src/contract_tests.rs b/packages/cw721/src/testing/contract_tests.rs similarity index 67% rename from contracts/cw721-base/src/contract_tests.rs rename to packages/cw721/src/testing/contract_tests.rs index b789748a8..2e4f4776f 100644 --- a/contracts/cw721-base/src/contract_tests.rs +++ b/packages/cw721/src/testing/contract_tests.rs @@ -1,32 +1,49 @@ #![cfg(test)] + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ from_json, to_json_binary, Addr, Coin, CosmosMsg, DepsMut, Empty, Response, StdError, WasmMsg, }; -use cw721::{ - Approval, ApprovalResponse, ContractInfoResponse, Cw721Query, Cw721ReceiveMsg, Expiration, - MinterResponse, NftInfoResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, +use crate::error::Cw721ContractError; +use crate::msg::{ + ApprovalResponse, NftInfoResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, }; -use cw_ownable::OwnershipError; +use crate::msg::{Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721QueryMsg}; +use crate::receiver::Cw721ReceiveMsg; +use crate::state::{CollectionInfo, DefaultOptionMetadataExtension, MINTER}; +use crate::{execute::Cw721Execute, query::Cw721Query, Approval, Expiration}; +use cw_ownable::{Action, Ownership, OwnershipError}; -use crate::{ContractError, Cw721Contract, ExecuteMsg, Extension, InstantiateMsg, QueryMsg}; +use super::contract::Cw721Contract; -const MINTER: &str = "merlin"; +const MINTER_ADDR: &str = "minter"; +const CREATOR_ADDR: &str = "creator"; const CONTRACT_NAME: &str = "Magic Power"; const SYMBOL: &str = "MGK"; -fn setup_contract(deps: DepsMut<'_>) -> Cw721Contract<'static, Extension, Empty, Empty, Empty> { +fn setup_contract( + deps: DepsMut<'_>, +) -> Cw721Contract<'static, DefaultOptionMetadataExtension, Empty, Empty, Empty> { let contract = Cw721Contract::default(); - let msg = InstantiateMsg { + let msg = Cw721InstantiateMsg { name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), - minter: Some(String::from(MINTER)), + minter: Some(String::from(MINTER_ADDR)), withdraw_address: None, }; let info = mock_info("creator", &[]); - let res = contract.instantiate(deps, mock_env(), info, msg).unwrap(); + let res = contract + .instantiate( + deps, + mock_env(), + info, + msg, + "contract_name", + "contract_version", + ) + .unwrap(); assert_eq!(0, res.messages.len()); contract } @@ -34,45 +51,120 @@ fn setup_contract(deps: DepsMut<'_>) -> Cw721Contract<'static, Extension, Empty, #[test] fn proper_instantiation() { let mut deps = mock_dependencies(); - let contract = Cw721Contract::::default(); + let contract = Cw721Contract::::default(); - let msg = InstantiateMsg { + let msg = Cw721InstantiateMsg { name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), - minter: Some(String::from(MINTER)), - withdraw_address: Some(String::from(MINTER)), + minter: Some(String::from(MINTER_ADDR)), + withdraw_address: Some(String::from(CREATOR_ADDR)), }; let info = mock_info("creator", &[]); + let env = mock_env(); // we can just call .unwrap() to assert this was a success let res = contract - .instantiate(deps.as_mut(), mock_env(), info, msg) + .instantiate( + deps.as_mut(), + env.clone(), + info, + msg, + "contract_name", + "contract_version", + ) .unwrap(); assert_eq!(0, res.messages.len()); // it worked, let's query the state - let res = contract.minter(deps.as_ref()).unwrap(); - assert_eq!(Some(MINTER.to_string()), res.minter); - let info = contract.contract_info(deps.as_ref()).unwrap(); + let minter_ownership = MINTER.get_ownership(deps.as_ref().storage).unwrap(); + assert_eq!(Some(Addr::unchecked(MINTER_ADDR)), minter_ownership.owner); + let collection_info = contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap(); + assert_eq!( + collection_info, + CollectionInfo { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + } + ); + + let withdraw_address = contract + .config + .withdraw_address + .may_load(deps.as_ref().storage) + .unwrap(); + assert_eq!(Some(CREATOR_ADDR.to_string()), withdraw_address); + + let count = contract + .query_num_tokens(deps.as_ref(), env.clone()) + .unwrap(); + assert_eq!(0, count.count); + + // list the token_ids + let tokens = contract + .query_all_tokens(deps.as_ref(), env, None, None) + .unwrap(); + assert_eq!(0, tokens.tokens.len()); +} + +#[test] +fn proper_instantiation_with_collection_info() { + let mut deps = mock_dependencies(); + let contract = Cw721Contract::::default(); + + let msg = Cw721InstantiateMsg { + name: CONTRACT_NAME.to_string(), + symbol: SYMBOL.to_string(), + minter: Some(String::from(MINTER_ADDR)), + withdraw_address: Some(String::from(CREATOR_ADDR)), + }; + let collection_info = mock_info("creator", &[]); + let env = mock_env(); + + // we can just call .unwrap() to assert this was a success + let res = contract + .instantiate( + deps.as_mut(), + env.clone(), + collection_info, + msg, + "contract_name", + "contract_version", + ) + .unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let minter_ownership = MINTER.get_ownership(deps.as_ref().storage).unwrap(); + assert_eq!(Some(Addr::unchecked(MINTER_ADDR)), minter_ownership.owner); + let info = contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!( info, - ContractInfoResponse { + CollectionInfo { name: CONTRACT_NAME.to_string(), symbol: SYMBOL.to_string(), } ); let withdraw_address = contract + .config .withdraw_address .may_load(deps.as_ref().storage) .unwrap(); - assert_eq!(Some(MINTER.to_string()), withdraw_address); + assert_eq!(Some(CREATOR_ADDR.to_string()), withdraw_address); - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let count = contract + .query_num_tokens(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!(0, count.count); // list the token_ids - let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env, None, None) + .unwrap(); assert_eq!(0, tokens.tokens.len()); } @@ -84,7 +176,7 @@ fn minting() { let token_id = "petrify".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("medusa"), token_uri: Some(token_uri.clone()), @@ -93,31 +185,36 @@ fn minting() { // random cannot mint let random = mock_info("random", &[]); + let env = mock_env(); let err = contract - .execute(deps.as_mut(), mock_env(), random, mint_msg.clone()) + .execute(deps.as_mut(), env.clone(), random, mint_msg.clone()) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); // minter can mint - let allowed = mock_info(MINTER, &[]); + let allowed = mock_info(MINTER_ADDR, &[]); let _ = contract - .execute(deps.as_mut(), mock_env(), allowed, mint_msg) + .execute(deps.as_mut(), env.clone(), allowed, mint_msg) .unwrap(); // ensure num tokens increases - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let count = contract + .query_num_tokens(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!(1, count.count); // unknown nft returns error let _ = contract - .nft_info(deps.as_ref(), "unknown".to_string()) + .query_nft_info(deps.as_ref(), env.clone(), "unknown".to_string()) .unwrap_err(); // this nft info is correct - let info = contract.nft_info(deps.as_ref(), token_id.clone()).unwrap(); + let info = contract + .query_nft_info(deps.as_ref(), env.clone(), token_id.clone()) + .unwrap(); assert_eq!( info, - NftInfoResponse:: { + NftInfoResponse:: { token_uri: Some(token_uri), extension: None, } @@ -125,7 +222,7 @@ fn minting() { // owner info is correct let owner = contract - .owner_of(deps.as_ref(), mock_env(), token_id.clone(), true) + .query_owner_of(deps.as_ref(), mock_env(), token_id.clone(), true) .unwrap(); assert_eq!( owner, @@ -136,21 +233,23 @@ fn minting() { ); // Cannot mint same token_id again - let mint_msg2 = ExecuteMsg::Mint { + let mint_msg2 = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("hercules"), token_uri: None, extension: None, }; - let allowed = mock_info(MINTER, &[]); + let allowed = mock_info(MINTER_ADDR, &[]); let err = contract .execute(deps.as_mut(), mock_env(), allowed, mint_msg2) .unwrap_err(); - assert_eq!(err, ContractError::Claimed {}); + assert_eq!(err, Cw721ContractError::Claimed {}); // list the token_ids - let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env, None, None) + .unwrap(); assert_eq!(1, tokens.tokens.len()); assert_eq!(vec![token_id], tokens.tokens); } @@ -163,7 +262,7 @@ fn test_update_minter() { let token_id = "petrify".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id, owner: String::from("medusa"), token_uri: Some(token_uri.clone()), @@ -171,7 +270,7 @@ fn test_update_minter() { }; // Minter can mint - let minter_info = mock_info(MINTER, &[]); + let minter_info = mock_info(MINTER_ADDR, &[]); let _ = contract .execute(deps.as_mut(), mock_env(), minter_info.clone(), mint_msg) .unwrap(); @@ -183,7 +282,7 @@ fn test_update_minter() { deps.as_mut(), mock_env(), minter_info.clone(), - ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + Cw721ExecuteMsg::UpdateOwnership(Action::TransferOwnership { new_owner: "random".to_string(), expiry: None, }), @@ -191,26 +290,18 @@ fn test_update_minter() { .unwrap(); // Minter does not change until ownership transfer completes. - let minter: MinterResponse = from_json( - contract - .query(deps.as_ref(), mock_env(), QueryMsg::Minter {}) - .unwrap(), - ) - .unwrap(); - assert_eq!(minter.minter, Some(MINTER.to_string())); - // Pending ownership transfer should be discoverable via query. - let ownership: cw_ownable::Ownership = from_json( + let ownership: Ownership = from_json( contract - .query(deps.as_ref(), mock_env(), QueryMsg::Ownership {}) + .query(deps.as_ref(), mock_env(), Cw721QueryMsg::Ownership {}) .unwrap(), ) .unwrap(); assert_eq!( ownership, - cw_ownable::Ownership:: { - owner: Some(Addr::unchecked(MINTER)), + Ownership:: { + owner: Some(Addr::unchecked(MINTER_ADDR)), pending_owner: Some(Addr::unchecked("random")), pending_expiry: None, } @@ -223,20 +314,20 @@ fn test_update_minter() { deps.as_mut(), mock_env(), random_info.clone(), - ExecuteMsg::UpdateOwnership(cw_ownable::Action::AcceptOwnership), + Cw721ExecuteMsg::UpdateOwnership(Action::AcceptOwnership), ) .unwrap(); // Minter changes after ownership transfer is accepted. - let minter: MinterResponse = from_json( + let minter_ownership: Ownership = from_json( contract - .query(deps.as_ref(), mock_env(), QueryMsg::Minter {}) + .query(deps.as_ref(), mock_env(), Cw721QueryMsg::Ownership {}) .unwrap(), ) .unwrap(); - assert_eq!(minter.minter, Some("random".to_string())); + assert_eq!(minter_ownership.owner, Some(random_info.sender.clone())); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: "randoms_token".to_string(), owner: String::from("medusa"), token_uri: Some(token_uri), @@ -244,10 +335,10 @@ fn test_update_minter() { }; // Old owner can not mint. - let err: ContractError = contract + let err: Cw721ContractError = contract .execute(deps.as_mut(), mock_env(), minter_info, mint_msg.clone()) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); // New owner can mint. let _ = contract @@ -263,44 +354,49 @@ fn burning() { let token_id = "petrify".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/petrify".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), - owner: MINTER.to_string(), + owner: MINTER_ADDR.to_string(), token_uri: Some(token_uri), extension: None, }; - let burn_msg = ExecuteMsg::Burn { token_id }; + let burn_msg = Cw721ExecuteMsg::Burn { token_id }; // mint some NFT - let allowed = mock_info(MINTER, &[]); + let allowed = mock_info(MINTER_ADDR, &[]); let _ = contract .execute(deps.as_mut(), mock_env(), allowed.clone(), mint_msg) .unwrap(); // random not allowed to burn let random = mock_info("random", &[]); + let env = mock_env(); let err = contract - .execute(deps.as_mut(), mock_env(), random, burn_msg.clone()) + .execute(deps.as_mut(), env.clone(), random, burn_msg.clone()) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); let _ = contract - .execute(deps.as_mut(), mock_env(), allowed, burn_msg) + .execute(deps.as_mut(), env.clone(), allowed, burn_msg) .unwrap(); // ensure num tokens decreases - let count = contract.num_tokens(deps.as_ref()).unwrap(); + let count = contract + .query_num_tokens(deps.as_ref(), env.clone()) + .unwrap(); assert_eq!(0, count.count); // trying to get nft returns error let _ = contract - .nft_info(deps.as_ref(), "petrify".to_string()) + .query_nft_info(deps.as_ref(), env.clone(), "petrify".to_string()) .unwrap_err(); // list the token_ids - let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env, None, None) + .unwrap(); assert!(tokens.tokens.is_empty()); } @@ -313,21 +409,21 @@ fn transferring_nft() { let token_id = "melt".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("venus"), token_uri: Some(token_uri), extension: None, }; - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), mock_env(), minter, mint_msg) .unwrap(); // random cannot transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("random"), token_id: token_id.clone(), }; @@ -335,11 +431,11 @@ fn transferring_nft() { let err = contract .execute(deps.as_mut(), mock_env(), random, transfer_msg) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); // owner can let random = mock_info("venus", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("random"), token_id: token_id.clone(), }; @@ -367,21 +463,21 @@ fn sending_nft() { let token_id = "melt".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/melt".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("venus"), token_uri: Some(token_uri), extension: None, }; - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), mock_env(), minter, mint_msg) .unwrap(); let msg = to_json_binary("You now have the melting power").unwrap(); let target = String::from("another_contract"); - let send_msg = ExecuteMsg::SendNft { + let send_msg = Cw721ExecuteMsg::SendNft { contract: target.clone(), token_id: token_id.clone(), msg: msg.clone(), @@ -391,7 +487,7 @@ fn sending_nft() { let err = contract .execute(deps.as_mut(), mock_env(), random, send_msg.clone()) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); // but owner can let random = mock_info("venus", &[]); @@ -433,21 +529,21 @@ fn approving_revoking() { let token_id = "grow".to_string(); let token_uri = "https://www.merriam-webster.com/dictionary/grow".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id.clone(), owner: String::from("demeter"), token_uri: Some(token_uri), extension: None, }; - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), mock_env(), minter, mint_msg) .unwrap(); // token owner shows in approval query let res = contract - .approval( + .query_approval( deps.as_ref(), mock_env(), token_id.clone(), @@ -459,14 +555,14 @@ fn approving_revoking() { res, ApprovalResponse { approval: Approval { - spender: String::from("demeter"), + spender: Addr::unchecked("demeter"), expires: Expiration::Never {} } } ); // Give random transferring power - let approve_msg = ExecuteMsg::Approve { + let approve_msg = Cw721ExecuteMsg::Approve { spender: String::from("random"), token_id: token_id.clone(), expires: None, @@ -486,7 +582,7 @@ fn approving_revoking() { // test approval query let res = contract - .approval( + .query_approval( deps.as_ref(), mock_env(), token_id.clone(), @@ -498,7 +594,7 @@ fn approving_revoking() { res, ApprovalResponse { approval: Approval { - spender: String::from("random"), + spender: Addr::unchecked("random"), expires: Expiration::Never {} } } @@ -506,7 +602,7 @@ fn approving_revoking() { // random can now transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("person"), token_id: token_id.clone(), }; @@ -515,7 +611,7 @@ fn approving_revoking() { .unwrap(); // Approvals are removed / cleared - let query_msg = QueryMsg::OwnerOf { + let query_msg = Cw721QueryMsg::OwnerOf { token_id: token_id.clone(), include_expired: None, }; @@ -534,7 +630,7 @@ fn approving_revoking() { ); // Approve, revoke, and check for empty, to test revoke - let approve_msg = ExecuteMsg::Approve { + let approve_msg = Cw721ExecuteMsg::Approve { spender: String::from("random"), token_id: token_id.clone(), expires: None, @@ -544,7 +640,7 @@ fn approving_revoking() { .execute(deps.as_mut(), mock_env(), owner.clone(), approve_msg) .unwrap(); - let revoke_msg = ExecuteMsg::Revoke { + let revoke_msg = Cw721ExecuteMsg::Revoke { spender: String::from("random"), token_id, }; @@ -580,41 +676,44 @@ fn approving_all_revoking_all() { let token_id2 = "grow2".to_string(); let token_uri2 = "https://www.merriam-webster.com/dictionary/grow2".to_string(); - let mint_msg1 = ExecuteMsg::Mint { + let mint_msg1 = Cw721ExecuteMsg::Mint { token_id: token_id1.clone(), owner: String::from("demeter"), token_uri: Some(token_uri1), extension: None, }; - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); contract .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg1) .unwrap(); - let mint_msg2 = ExecuteMsg::Mint { + let mint_msg2 = Cw721ExecuteMsg::Mint { token_id: token_id2.clone(), owner: String::from("demeter"), token_uri: Some(token_uri2), extension: None, }; + let env = mock_env(); contract - .execute(deps.as_mut(), mock_env(), minter, mint_msg2) + .execute(deps.as_mut(), env.clone(), minter, mint_msg2) .unwrap(); // paginate the token_ids - let tokens = contract.all_tokens(deps.as_ref(), None, Some(1)).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, Some(1)) + .unwrap(); assert_eq!(1, tokens.tokens.len()); assert_eq!(vec![token_id1.clone()], tokens.tokens); let tokens = contract - .all_tokens(deps.as_ref(), Some(token_id1.clone()), Some(3)) + .query_all_tokens(deps.as_ref(), env, Some(token_id1.clone()), Some(3)) .unwrap(); assert_eq!(1, tokens.tokens.len()); assert_eq!(vec![token_id2.clone()], tokens.tokens); // demeter gives random full (operator) power over her tokens - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("random"), expires: None, }; @@ -632,7 +731,7 @@ fn approving_all_revoking_all() { // random can now transfer let random = mock_info("random", &[]); - let transfer_msg = ExecuteMsg::TransferNft { + let transfer_msg = Cw721ExecuteMsg::TransferNft { recipient: String::from("person"), token_id: token_id1, }; @@ -648,7 +747,7 @@ fn approving_all_revoking_all() { }; let msg: CosmosMsg = CosmosMsg::Wasm(inner_msg); - let send_msg = ExecuteMsg::SendNft { + let send_msg = Cw721ExecuteMsg::SendNft { contract: String::from("another_contract"), token_id: token_id2, msg: to_json_binary(&msg).unwrap(), @@ -658,7 +757,7 @@ fn approving_all_revoking_all() { .unwrap(); // Approve_all, revoke_all, and check for empty, to test revoke_all - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("operator"), expires: None, }; @@ -670,7 +769,7 @@ fn approving_all_revoking_all() { // query for operator should return approval let res = contract - .operator( + .query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -682,14 +781,14 @@ fn approving_all_revoking_all() { res, OperatorResponse { approval: Approval { - spender: String::from("operator"), + spender: Addr::unchecked("operator"), expires: Expiration::Never {} } } ); // query for other should throw error - let res = contract.operator( + let res = contract.query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -702,7 +801,7 @@ fn approving_all_revoking_all() { } let res = contract - .operators( + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -714,8 +813,8 @@ fn approving_all_revoking_all() { assert_eq!( res, OperatorsResponse { - operators: vec![cw721::Approval { - spender: String::from("operator"), + operators: vec![Approval { + spender: Addr::unchecked("operator"), expires: Expiration::Never {} }] } @@ -723,7 +822,7 @@ fn approving_all_revoking_all() { // second approval let buddy_expires = Expiration::AtHeight(1234567); - let approve_all_msg = ExecuteMsg::ApproveAll { + let approve_all_msg = Cw721ExecuteMsg::ApproveAll { operator: String::from("buddy"), expires: Some(buddy_expires), }; @@ -734,7 +833,7 @@ fn approving_all_revoking_all() { // and paginate queries let res = contract - .operators( + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -746,14 +845,14 @@ fn approving_all_revoking_all() { assert_eq!( res, OperatorsResponse { - operators: vec![cw721::Approval { - spender: String::from("buddy"), + operators: vec![Approval { + spender: Addr::unchecked("buddy"), expires: buddy_expires, }] } ); let res = contract - .operators( + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -765,14 +864,14 @@ fn approving_all_revoking_all() { assert_eq!( res, OperatorsResponse { - operators: vec![cw721::Approval { - spender: String::from("operator"), + operators: vec![Approval { + spender: Addr::unchecked("operator"), expires: Expiration::Never {} }] } ); - let revoke_all_msg = ExecuteMsg::RevokeAll { + let revoke_all_msg = Cw721ExecuteMsg::RevokeAll { operator: String::from("operator"), }; contract @@ -780,7 +879,7 @@ fn approving_all_revoking_all() { .unwrap(); // query for operator should return error - let res = contract.operator( + let res = contract.query_operator( deps.as_ref(), mock_env(), String::from("person"), @@ -794,7 +893,7 @@ fn approving_all_revoking_all() { // Approvals are removed / cleared without affecting others let res = contract - .operators( + .query_operators( deps.as_ref(), mock_env(), String::from("person"), @@ -806,8 +905,8 @@ fn approving_all_revoking_all() { assert_eq!( res, OperatorsResponse { - operators: vec![cw721::Approval { - spender: String::from("buddy"), + operators: vec![Approval { + spender: Addr::unchecked("buddy"), expires: buddy_expires, }] } @@ -817,7 +916,7 @@ fn approving_all_revoking_all() { let mut late_env = mock_env(); late_env.block.height = 1234568; //expired let res = contract - .operators( + .query_operators( deps.as_ref(), late_env.clone(), String::from("person"), @@ -829,7 +928,7 @@ fn approving_all_revoking_all() { assert_eq!(0, res.operators.len()); // query operator should also return error - let res = contract.operator( + let res = contract.query_operator( deps.as_ref(), late_env, String::from("person"), @@ -848,18 +947,23 @@ fn test_set_withdraw_address() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut()); - // other cant set + // other than minter cant set let err = contract .set_withdraw_address(deps.as_mut(), &Addr::unchecked("other"), "foo".to_string()) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); // minter can set contract - .set_withdraw_address(deps.as_mut(), &Addr::unchecked(MINTER), "foo".to_string()) + .set_withdraw_address( + deps.as_mut(), + &Addr::unchecked(MINTER_ADDR), + "foo".to_string(), + ) .unwrap(); let withdraw_address = contract + .config .withdraw_address .load(deps.as_ref().storage) .unwrap(); @@ -871,32 +975,44 @@ fn test_remove_withdraw_address() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut()); - // other cant remove + // other than creator cant remove let err = contract .remove_withdraw_address(deps.as_mut().storage, &Addr::unchecked("other")) .unwrap_err(); - assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner)); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); - // no owner set yet + // no withdraw address set yet let err = contract - .remove_withdraw_address(deps.as_mut().storage, &Addr::unchecked(MINTER)) + .remove_withdraw_address(deps.as_mut().storage, &Addr::unchecked(MINTER_ADDR)) .unwrap_err(); - assert_eq!(err, ContractError::NoWithdrawAddress {}); + assert_eq!(err, Cw721ContractError::NoWithdrawAddress {}); // set and remove contract - .set_withdraw_address(deps.as_mut(), &Addr::unchecked(MINTER), "foo".to_string()) + .set_withdraw_address( + deps.as_mut(), + &Addr::unchecked(MINTER_ADDR), + "foo".to_string(), + ) .unwrap(); contract - .remove_withdraw_address(deps.as_mut().storage, &Addr::unchecked(MINTER)) + .remove_withdraw_address(deps.as_mut().storage, &Addr::unchecked(MINTER_ADDR)) .unwrap(); - assert!(!contract.withdraw_address.exists(deps.as_ref().storage)); + assert!(!contract + .config + .withdraw_address + .exists(deps.as_ref().storage)); // test that we can set again contract - .set_withdraw_address(deps.as_mut(), &Addr::unchecked(MINTER), "foo".to_string()) + .set_withdraw_address( + deps.as_mut(), + &Addr::unchecked(MINTER_ADDR), + "foo".to_string(), + ) .unwrap(); let withdraw_address = contract + .config .withdraw_address .load(deps.as_ref().storage) .unwrap(); @@ -912,11 +1028,15 @@ fn test_withdraw_funds() { let err = contract .withdraw_funds(deps.as_mut().storage, &Coin::new(100, "uark")) .unwrap_err(); - assert_eq!(err, ContractError::NoWithdrawAddress {}); + assert_eq!(err, Cw721ContractError::NoWithdrawAddress {}); // set and withdraw by non-owner contract - .set_withdraw_address(deps.as_mut(), &Addr::unchecked(MINTER), "foo".to_string()) + .set_withdraw_address( + deps.as_mut(), + &Addr::unchecked(MINTER_ADDR), + "foo".to_string(), + ) .unwrap(); contract .withdraw_funds(deps.as_mut().storage, &Coin::new(100, "uark")) @@ -927,7 +1047,7 @@ fn test_withdraw_funds() { fn query_tokens_by_owner() { let mut deps = mock_dependencies(); let contract = setup_contract(deps.as_mut()); - let minter = mock_info(MINTER, &[]); + let minter = mock_info(MINTER_ADDR, &[]); // Mint a couple tokens (from the same owner) let token_id1 = "grow1".to_string(); @@ -936,7 +1056,7 @@ fn query_tokens_by_owner() { let ceres = String::from("ceres"); let token_id3 = "sing".to_string(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id1.clone(), owner: demeter.clone(), token_uri: None, @@ -946,7 +1066,7 @@ fn query_tokens_by_owner() { .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) .unwrap(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id2.clone(), owner: ceres.clone(), token_uri: None, @@ -956,25 +1076,30 @@ fn query_tokens_by_owner() { .execute(deps.as_mut(), mock_env(), minter.clone(), mint_msg) .unwrap(); - let mint_msg = ExecuteMsg::Mint { + let mint_msg = Cw721ExecuteMsg::Mint { token_id: token_id3.clone(), owner: demeter.clone(), token_uri: None, extension: None, }; + let env = mock_env(); contract - .execute(deps.as_mut(), mock_env(), minter, mint_msg) + .execute(deps.as_mut(), env.clone(), minter, mint_msg) .unwrap(); // get all tokens in order: let expected = vec![token_id1.clone(), token_id2.clone(), token_id3.clone()]; - let tokens = contract.all_tokens(deps.as_ref(), None, None).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, None) + .unwrap(); assert_eq!(&expected, &tokens.tokens); // paginate - let tokens = contract.all_tokens(deps.as_ref(), None, Some(2)).unwrap(); + let tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, Some(2)) + .unwrap(); assert_eq!(&expected[..2], &tokens.tokens[..]); let tokens = contract - .all_tokens(deps.as_ref(), Some(expected[1].clone()), None) + .query_all_tokens(deps.as_ref(), env.clone(), Some(expected[1].clone()), None) .unwrap(); assert_eq!(&expected[2..], &tokens.tokens[..]); @@ -983,19 +1108,27 @@ fn query_tokens_by_owner() { let by_demeter = vec![token_id1, token_id3]; // all tokens by owner let tokens = contract - .tokens(deps.as_ref(), demeter.clone(), None, None) + .query_tokens(deps.as_ref(), env.clone(), demeter.clone(), None, None) .unwrap(); assert_eq!(&by_demeter, &tokens.tokens); - let tokens = contract.tokens(deps.as_ref(), ceres, None, None).unwrap(); + let tokens = contract + .query_tokens(deps.as_ref(), env.clone(), ceres, None, None) + .unwrap(); assert_eq!(&by_ceres, &tokens.tokens); // paginate for demeter let tokens = contract - .tokens(deps.as_ref(), demeter.clone(), None, Some(1)) + .query_tokens(deps.as_ref(), env.clone(), demeter.clone(), None, Some(1)) .unwrap(); assert_eq!(&by_demeter[..1], &tokens.tokens[..]); let tokens = contract - .tokens(deps.as_ref(), demeter, Some(by_demeter[0].clone()), Some(3)) + .query_tokens( + deps.as_ref(), + env, + demeter, + Some(by_demeter[0].clone()), + Some(3), + ) .unwrap(); assert_eq!(&by_demeter[1..], &tokens.tokens[..]); } diff --git a/packages/cw721/src/testing/mod.rs b/packages/cw721/src/testing/mod.rs new file mode 100644 index 000000000..b94a351ae --- /dev/null +++ b/packages/cw721/src/testing/mod.rs @@ -0,0 +1,4 @@ +mod contract; +mod contract_tests; +mod multi_tests; +mod unit_tests; diff --git a/packages/cw721/src/testing/multi_tests.rs b/packages/cw721/src/testing/multi_tests.rs new file mode 100644 index 000000000..ed01666f8 --- /dev/null +++ b/packages/cw721/src/testing/multi_tests.rs @@ -0,0 +1,941 @@ +use crate::{ + error::Cw721ContractError, + execute::Cw721Execute, + msg::{ + Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721MigrateMsg, Cw721QueryMsg, MinterResponse, + OwnerOfResponse, + }, + query::Cw721Query, + state::DefaultOptionMetadataExtension, +}; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, QuerierWrapper, Response, + StdResult, WasmMsg, +}; +use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cw_ownable::{Ownership, OwnershipError}; +use cw_utils::Expiration; + +use super::contract::Cw721Contract; + +pub const CREATOR_ADDR: &str = "creator"; +pub const MINTER_ADDR: &str = "minter"; +pub const OTHER_ADDR: &str = "other"; +pub const NFT_OWNER_ADDR: &str = "nft_owner"; + +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721InstantiateMsg, +) -> Result { + let contract = Cw721Contract::::default(); + contract.instantiate(deps, env, info, msg, "contract_name", "contract_version") +} + +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721ExecuteMsg, +) -> Result { + let contract = Cw721Contract::::default(); + contract.execute(deps, env, info, msg) +} + +pub fn query( + deps: Deps, + env: Env, + msg: Cw721QueryMsg, +) -> StdResult { + let contract = Cw721Contract::::default(); + contract.query(deps, env, msg) +} + +pub fn migrate( + deps: DepsMut, + env: Env, + msg: Cw721MigrateMsg, +) -> Result { + let contract = Cw721Contract::::default(); + contract.migrate(deps, env, msg, "contract_name", "contract_version") +} + +fn cw721_base_latest_contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); + Box::new(contract) +} + +fn cw721_base_016_contract() -> Box> { + use cw721_base_016 as v16; + let contract = ContractWrapper::new( + v16::entry::execute, + v16::entry::instantiate, + v16::entry::query, + ); + Box::new(contract) +} + +fn cw721_base_017_contract() -> Box> { + use cw721_base_017 as v17; + let contract = ContractWrapper::new( + v17::entry::execute, + v17::entry::instantiate, + v17::entry::query, + ); + Box::new(contract) +} + +fn cw721_base_018_contract() -> Box> { + use cw721_base_018 as v18; + let contract = ContractWrapper::new( + v18::entry::execute, + v18::entry::instantiate, + v18::entry::query, + ); + Box::new(contract) +} + +fn query_owner(querier: QuerierWrapper, cw721: &Addr, token_id: String) -> Addr { + let resp: OwnerOfResponse = querier + .query_wasm_smart( + cw721, + &Cw721QueryMsg::::OwnerOf { + token_id, + include_expired: None, + }, + ) + .unwrap(); + Addr::unchecked(resp.owner) +} + +fn mint_transfer_and_burn(app: &mut App, cw721: Addr, sender: Addr, token_id: String) { + app.execute_contract( + sender.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: token_id.clone(), + owner: sender.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap(); + + let owner = query_owner(app.wrap(), &cw721, token_id.clone()); + assert_eq!(owner, sender.to_string()); + + app.execute_contract( + sender, + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: "burner".to_string(), + token_id: token_id.clone(), + }, + &[], + ) + .unwrap(); + + let owner = query_owner(app.wrap(), &cw721, token_id.clone()); + assert_eq!(owner, "burner".to_string()); + + app.execute_contract( + Addr::unchecked("burner"), + cw721, + &Cw721ExecuteMsg::::Burn { token_id }, + &[], + ) + .unwrap(); +} + +#[test] +fn test_operator() { + // --- setup --- + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + let code_id = app.store_code(cw721_base_latest_contract()); + let other = Addr::unchecked(OTHER_ADDR); + let cw721 = app + .instantiate_contract( + code_id, + other.clone(), + &Cw721InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: Some(MINTER_ADDR.to_string()), + withdraw_address: None, + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + // mint + let minter = Addr::unchecked(MINTER_ADDR); + let nft_owner = Addr::unchecked(NFT_OWNER_ADDR); + app.execute_contract( + minter, + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: nft_owner.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap(); + + // --- test operator/approve all --- + // owner adds other user as operator using approve all + app.execute_contract( + nft_owner.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::ApproveAll { + operator: other.to_string(), + expires: Some(Expiration::Never {}), + }, + &[], + ) + .unwrap(); + + // transfer by operator + app.execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: other.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap(); + // check other is new owner + let owner_response: OwnerOfResponse = app + .wrap() + .query_wasm_smart( + &cw721, + &Cw721QueryMsg::::OwnerOf { + token_id: "1".to_string(), + include_expired: None, + }, + ) + .unwrap(); + assert_eq!(owner_response.owner, other.to_string()); + // check previous owner cant transfer + let err: Cw721ContractError = app + .execute_contract( + nft_owner.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: other.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // transfer back to previous owner + app.execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: nft_owner.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap(); + // check owner + let owner_response: OwnerOfResponse = app + .wrap() + .query_wasm_smart( + &cw721, + &Cw721QueryMsg::::OwnerOf { + token_id: "1".to_string(), + include_expired: None, + }, + ) + .unwrap(); + assert_eq!(owner_response.owner, nft_owner.to_string()); + + // other user is still operator and can transfer! + app.execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: other.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap(); + // check other is new owner + let owner_response: OwnerOfResponse = app + .wrap() + .query_wasm_smart( + &cw721, + &Cw721QueryMsg::::OwnerOf { + token_id: "1".to_string(), + include_expired: None, + }, + ) + .unwrap(); + assert_eq!(owner_response.owner, other.to_string()); + + // -- test revoke + // transfer to previous owner + app.execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::TransferNft { + recipient: nft_owner.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap(); + + // revoke operator + app.execute_contract( + nft_owner, + cw721.clone(), + &Cw721ExecuteMsg::::RevokeAll { + operator: other.to_string(), + }, + &[], + ) + .unwrap(); + + // other not operator anymore and cant send + let err: Cw721ContractError = app + .execute_contract( + other.clone(), + cw721, + &Cw721ExecuteMsg::::TransferNft { + recipient: other.to_string(), + token_id: "1".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); +} + +/// Instantiates a 0.16 version of this contract and tests that tokens +/// can be minted, transferred, and burnred after migration. +#[test] +fn test_migration_legacy_to_latest() { + // case 1: migrate from v0.16 to latest by using existing minter addr + { + use cw721_base_016 as v16; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_016 = app.store_code(cw721_base_016_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_016, + legacy_creator_and_minter.clone(), + &v16::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: None, + creator: None, + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // non-minter user cant mint + let other = Addr::unchecked(OTHER_ADDR); + let err: Cw721ContractError = app + .execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: other.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // legacy minter can still mint + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(legacy_creator_and_minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v16::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, legacy_creator_and_minter.to_string()); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(legacy_creator_and_minter)); + } + // case 2: migrate from v0.16 to latest by providing new creator and minter addr + { + use cw721_base_016 as v16; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_016 = app.store_code(cw721_base_016_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_016, + legacy_creator_and_minter.clone(), + &v16::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: Some(MINTER_ADDR.to_string()), + creator: Some(CREATOR_ADDR.to_string()), + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // legacy minter user cant mint + let err: Cw721ContractError = app + .execute_contract( + legacy_creator_and_minter.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: legacy_creator_and_minter.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // new minter can mint + let minter = Addr::unchecked(MINTER_ADDR); + mint_transfer_and_burn(&mut app, cw721.clone(), minter.clone(), "1".to_string()); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v16::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, minter.to_string()); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(minter)); + } + // case 3: migrate from v0.17 to latest by using existing minter addr + { + use cw721_base_017 as v17; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_017 = app.store_code(cw721_base_017_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_017, + legacy_creator_and_minter.clone(), + &v17::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: None, + creator: None, + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // non-minter user cant mint + let other = Addr::unchecked(OTHER_ADDR); + let err: Cw721ContractError = app + .execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: other.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // legacy minter can still mint + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(legacy_creator_and_minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v17::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(legacy_creator_and_minter.to_string())); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(legacy_creator_and_minter)); + } + // case 4: migrate from v0.17 to latest by providing new creator and minter addr + { + use cw721_base_017 as v17; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_017 = app.store_code(cw721_base_017_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_017, + legacy_creator_and_minter.clone(), + &v17::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: Some(MINTER_ADDR.to_string()), + creator: Some(CREATOR_ADDR.to_string()), + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // legacy minter user cant mint + let err: Cw721ContractError = app + .execute_contract( + legacy_creator_and_minter.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: legacy_creator_and_minter.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // new minter can mint + let minter = Addr::unchecked(MINTER_ADDR); + mint_transfer_and_burn(&mut app, cw721.clone(), minter.clone(), "1".to_string()); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v17::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(minter.to_string())); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(minter)); + } + // case 5: migrate from v0.18 to latest by using existing minter addr + { + use cw721_base_018 as v18; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_018 = app.store_code(cw721_base_018_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_018, + legacy_creator_and_minter.clone(), + &v18::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: None, + creator: None, + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // non-minter user cant mint + let other = Addr::unchecked(OTHER_ADDR); + let err: Cw721ContractError = app + .execute_contract( + other.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: other.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // legacy minter can still mint + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(legacy_creator_and_minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v18::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(legacy_creator_and_minter.to_string())); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(legacy_creator_and_minter)); + } + // case 6: migrate from v0.18 to latest by providing new creator and minter addr + { + use cw721_base_018 as v18; + let mut app = App::default(); + let admin = Addr::unchecked("admin"); + + let code_id_018 = app.store_code(cw721_base_018_contract()); + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let legacy_creator_and_minter = Addr::unchecked("legacy_creator_and_minter"); + + let cw721 = app + .instantiate_contract( + code_id_018, + legacy_creator_and_minter.clone(), + &v18::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: legacy_creator_and_minter.to_string(), + }, + &[], + "cw721-base", + Some(admin.to_string()), + ) + .unwrap(); + + mint_transfer_and_burn( + &mut app, + cw721.clone(), + legacy_creator_and_minter.clone(), + "1".to_string(), + ); + + // migrate + app.execute( + admin, + WasmMsg::Migrate { + contract_addr: cw721.to_string(), + new_code_id: code_id_latest, + msg: to_json_binary(&Cw721MigrateMsg::WithUpdate { + minter: Some(MINTER_ADDR.to_string()), + creator: Some(CREATOR_ADDR.to_string()), + }) + .unwrap(), + } + .into(), + ) + .unwrap(); + + // legacy minter user cant mint + let err: Cw721ContractError = app + .execute_contract( + legacy_creator_and_minter.clone(), + cw721.clone(), + &Cw721ExecuteMsg::::Mint { + token_id: "1".to_string(), + owner: legacy_creator_and_minter.to_string(), + token_uri: None, + extension: Empty::default(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, Cw721ContractError::Ownership(OwnershipError::NotOwner)); + + // new minter can mint + let minter = Addr::unchecked(MINTER_ADDR); + mint_transfer_and_burn(&mut app, cw721.clone(), minter.clone(), "1".to_string()); + + // check new mint query response works. + let m: MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(minter.to_string())); + + // check that the new response is backwards compatable when minter + // is not None. + let m: v18::MinterResponse = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Minter {}) + .unwrap(); + assert_eq!(m.minter, Some(minter.to_string())); + + // check minter ownership query works + let minter_ownership: Ownership = app + .wrap() + .query_wasm_smart(&cw721, &Cw721QueryMsg::::Ownership {}) + .unwrap(); + assert_eq!(minter_ownership.owner, Some(minter)); + } +} + +/// Test backward compatibility using instantiate msg from a 0.16 version on latest contract. +/// This ensures existing 3rd party contracts doesnt need to update as well. +#[test] +fn test_instantiate_016_msg() { + use cw721_base_016 as v16; + let mut app = App::default(); + let admin = || Addr::unchecked("admin"); + + let code_id_latest = app.store_code(cw721_base_latest_contract()); + + let cw721 = app + .instantiate_contract( + code_id_latest, + admin(), + &v16::InstantiateMsg { + name: "collection".to_string(), + symbol: "symbol".to_string(), + minter: admin().into_string(), + }, + &[], + "cw721-base", + Some(admin().into_string()), + ) + .unwrap(); + + // assert withdraw address is None + let withdraw_addr: Option = app + .wrap() + .query_wasm_smart(cw721, &Cw721QueryMsg::::GetWithdrawAddress {}) + .unwrap(); + assert!(withdraw_addr.is_none()); +} diff --git a/packages/cw721/src/testing/unit_tests.rs b/packages/cw721/src/testing/unit_tests.rs new file mode 100644 index 000000000..79b84eab2 --- /dev/null +++ b/packages/cw721/src/testing/unit_tests.rs @@ -0,0 +1,255 @@ +use crate::{ + execute::Cw721Execute, + msg::{Cw721ExecuteMsg, Cw721InstantiateMsg}, + query::{Cw721Query, MAX_LIMIT}, + state::{CollectionInfo, DefaultOptionMetadataExtension, Metadata, MINTER}, +}; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, Empty, +}; +use cw2::ContractVersion; +use cw_storage_plus::Item; +use unit_tests::{contract::Cw721Contract, multi_tests::CREATOR_ADDR}; + +use super::*; + +/// Make sure cw2 version info is properly initialized during instantiation. +#[test] +fn proper_cw2_initialization() { + let mut deps = mock_dependencies(); + + Cw721Contract::::default() + .instantiate( + deps.as_mut(), + mock_env(), + mock_info("larry", &[]), + Cw721InstantiateMsg { + name: "collection_name".into(), + symbol: "collection_symbol".into(), + minter: Some("minter".into()), + withdraw_address: None, + }, + "contract_name", + "contract_version", + ) + .unwrap(); + + let minter = MINTER + .get_ownership(deps.as_ref().storage) + .unwrap() + .owner + .map(|a| a.into_string()); + assert_eq!(minter, Some("minter".to_string())); + + let version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); + assert_eq!( + version, + ContractVersion { + contract: "contract_name".into(), + version: "contract_version".into(), + }, + ); +} + +#[test] +fn proper_owner_initialization() { + let mut deps = mock_dependencies(); + + let info_owner = mock_info("owner", &[]); + Cw721Contract::::default() + .instantiate( + deps.as_mut(), + mock_env(), + info_owner.clone(), + Cw721InstantiateMsg { + name: "collection_name".into(), + symbol: "collection_symbol".into(), + minter: None, + withdraw_address: None, + }, + "contract_name", + "contract_version", + ) + .unwrap(); + + let minter = MINTER.item.load(deps.as_ref().storage).unwrap().owner; + assert_eq!(minter, Some(info_owner.sender)); +} + +#[test] +fn use_metadata_extension() { + let mut deps = mock_dependencies(); + let contract = Cw721Contract::::default(); + + let info = mock_info(CREATOR_ADDR, &[]); + let init_msg = Cw721InstantiateMsg { + name: "collection_name".into(), + symbol: "collection_symbol".into(), + minter: None, + withdraw_address: None, + }; + let env = mock_env(); + contract + .instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + init_msg, + "contract_name", + "contract_version", + ) + .unwrap(); + + let token_id = "Enterprise"; + let token_uri = Some("https://starships.example.com/Starship/Enterprise.json".into()); + let extension = Some(Metadata { + description: Some("Spaceship with Warp Drive".into()), + name: Some("Starship USS Enterprise".to_string()), + ..Metadata::default() + }); + let exec_msg = Cw721ExecuteMsg::Mint { + token_id: token_id.to_string(), + owner: "john".to_string(), + token_uri: token_uri.clone(), + extension: extension.clone(), + }; + contract + .execute(deps.as_mut(), env.clone(), info, exec_msg) + .unwrap(); + + let res = contract + .query_nft_info(deps.as_ref(), env, token_id.into()) + .unwrap(); + assert_eq!(res.token_uri, token_uri); + assert_eq!(res.extension, extension); +} + +#[test] +fn test_migrate() { + let mut deps = mock_dependencies(); + + let env = mock_env(); + use cw721_base_016 as v16; + v16::entry::instantiate( + deps.as_mut(), + env.clone(), + mock_info("owner", &[]), + v16::InstantiateMsg { + name: "legacy_name".into(), + symbol: "legacy_symbol".into(), + minter: "legacy_minter".into(), + }, + ) + .unwrap(); + + // mint 200 NFTs before migration + for i in 0..200 { + let info = mock_info("legacy_minter", &[]); + let msg = v16::ExecuteMsg::Mint(v16::msg::MintMsg { + token_id: i.to_string(), + owner: "owner".into(), + token_uri: None, + extension: None, + }); + v16::entry::execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + } + + // assert new data before migration: + // - ownership and collection info throws NotFound Error + MINTER.item.load(deps.as_ref().storage).unwrap_err(); // cw_ownable in v16 is used for minter + let contract = Cw721Contract::::default(); + contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap_err(); + // - query in new minter and creator ownership store throws NotFound Error (in v16 it was stored outside cw_ownable, in dedicated "minter" store) + MINTER.get_ownership(deps.as_ref().storage).unwrap_err(); + // assert legacy data before migration: + // - version + let version = cw2::get_contract_version(deps.as_ref().storage) + .unwrap() + .version; + assert_eq!(version, "0.16.0"); + // - legacy minter is set + let legacy_minter_store: Item = Item::new("minter"); + let legacy_minter = legacy_minter_store.load(deps.as_ref().storage).unwrap(); + assert_eq!(legacy_minter, "legacy_minter"); + // - legacy collection info is set + let legacy_collection_info_store: Item = Item::new("nft_info"); + let all_tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, Some(MAX_LIMIT)) + .unwrap(); + assert_eq!(all_tokens.tokens.len(), 200); + for token_id in 0..200 { + let token = contract + .query_owner_of(deps.as_ref(), env.clone(), token_id.to_string(), false) + .unwrap(); + assert_eq!(token.owner.as_str(), "owner"); + } + + Cw721Contract::::default() + .migrate( + deps.as_mut(), + env.clone(), + crate::msg::Cw721MigrateMsg::WithUpdate { + minter: None, + creator: None, + }, + "contract_name", + "contract_version", + ) + .unwrap(); + + // version + let version = cw2::get_contract_version(deps.as_ref().storage) + .unwrap() + .version; + assert_eq!(version, "contract_version"); + assert_ne!(version, "0.16.0"); + + // assert minter ownership + let minter_ownership = MINTER + .get_ownership(deps.as_ref().storage) + .unwrap() + .owner + .map(|a| a.into_string()); + assert_eq!(minter_ownership, Some("legacy_minter".to_string())); + + // assert collection info + let collection_info = contract + .query_collection_info(deps.as_ref(), env.clone()) + .unwrap(); + let legacy_contract_info = CollectionInfo { + name: "legacy_name".to_string(), + symbol: "legacy_symbol".to_string(), + }; + assert_eq!(collection_info, legacy_contract_info); + + // assert tokens + let all_tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, Some(MAX_LIMIT)) + .unwrap(); + assert_eq!(all_tokens.tokens.len(), 200); + + // assert legacy data is still there (allowing backward migration in case of issues) + // - minter + let legacy_minter = legacy_minter_store.load(deps.as_ref().storage).unwrap(); + assert_eq!(legacy_minter, "legacy_minter"); + // - collection info + let legacy_collection_info = legacy_collection_info_store + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(legacy_collection_info.name, "legacy_name"); + assert_eq!(legacy_collection_info.symbol, "legacy_symbol"); + // - tokens are unchanged/still exist + let all_tokens = contract + .query_all_tokens(deps.as_ref(), env.clone(), None, Some(MAX_LIMIT)) + .unwrap(); + assert_eq!(all_tokens.tokens.len(), 200); + for token_id in 0..200 { + let token = contract + .query_owner_of(deps.as_ref(), env.clone(), token_id.to_string(), false) + .unwrap(); + assert_eq!(token.owner.as_str(), "owner"); + } +} diff --git a/packages/cw721/src/traits.rs b/packages/cw721/src/traits.rs deleted file mode 100644 index 1022a0eb3..000000000 --- a/packages/cw721/src/traits.rs +++ /dev/null @@ -1,168 +0,0 @@ -use serde::de::DeserializeOwned; -use serde::Serialize; - -use crate::{ - AllNftInfoResponse, ApprovalResponse, ApprovalsResponse, ContractInfoResponse, NftInfoResponse, - NumTokensResponse, OperatorResponse, OperatorsResponse, OwnerOfResponse, TokensResponse, -}; -use cosmwasm_std::{Binary, CustomMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; -use cw_utils::Expiration; - -pub trait Cw721: Cw721Execute + Cw721Query -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, -{ -} - -pub trait Cw721Execute -where - T: Serialize + DeserializeOwned + Clone, - C: CustomMsg, -{ - type Err: ToString; - - fn transfer_nft( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - recipient: String, - token_id: String, - ) -> Result, Self::Err>; - - fn send_nft( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - contract: String, - token_id: String, - msg: Binary, - ) -> Result, Self::Err>; - - fn approve( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - token_id: String, - expires: Option, - ) -> Result, Self::Err>; - - fn revoke( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - spender: String, - token_id: String, - ) -> Result, Self::Err>; - - fn approve_all( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - operator: String, - expires: Option, - ) -> Result, Self::Err>; - - fn revoke_all( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - operator: String, - ) -> Result, Self::Err>; - - fn burn( - &self, - deps: DepsMut, - env: Env, - info: MessageInfo, - token_id: String, - ) -> Result, Self::Err>; -} - -pub trait Cw721Query -where - T: Serialize + DeserializeOwned + Clone, -{ - // TODO: use custom error? - // How to handle the two derived error types? - - fn contract_info(&self, deps: Deps) -> StdResult; - - fn num_tokens(&self, deps: Deps) -> StdResult; - - fn nft_info(&self, deps: Deps, token_id: String) -> StdResult>; - - fn owner_of( - &self, - deps: Deps, - env: Env, - token_id: String, - include_expired: bool, - ) -> StdResult; - - fn operator( - &self, - deps: Deps, - env: Env, - owner: String, - operator: String, - include_expired: bool, - ) -> StdResult; - - fn operators( - &self, - deps: Deps, - env: Env, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, - ) -> StdResult; - - fn approval( - &self, - deps: Deps, - env: Env, - token_id: String, - spender: String, - include_expired: bool, - ) -> StdResult; - - fn approvals( - &self, - deps: Deps, - env: Env, - token_id: String, - include_expired: bool, - ) -> StdResult; - - fn tokens( - &self, - deps: Deps, - owner: String, - start_after: Option, - limit: Option, - ) -> StdResult; - - fn all_tokens( - &self, - deps: Deps, - start_after: Option, - limit: Option, - ) -> StdResult; - - fn all_nft_info( - &self, - deps: Deps, - env: Env, - token_id: String, - include_expired: bool, - ) -> StdResult>; -}