diff --git a/Cargo.lock b/Cargo.lock index 1191295..136b6a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1021,6 +1021,7 @@ dependencies = [ "cw-hooks", "dao-interface", "dao-voting 2.4.0", + "query_auth 0.1.0", "schemars 0.8.16", "secret-cosmwasm-std", "secret-cw2", @@ -1066,10 +1067,13 @@ dependencies = [ "dao-pre-propose-base", "dao-proposal-single", "dao-voting 2.4.0", + "query_auth 0.1.0", "secret-cosmwasm-std", "secret-cw2", "secret-multi-test", "secret-utils 0.13.4", + "shade-protocol", + "snip20-reference-impl", ] [[package]] @@ -1154,7 +1158,6 @@ dependencies = [ "dao-hooks", "dao-interface", "dao-pre-propose-base", - "dao-pre-propose-single", "dao-testing", "dao-voting 2.4.0", "dao-voting-cw4", @@ -1328,8 +1331,8 @@ dependencies = [ "serde", "shade-protocol", "snip721-controllers", - "snip721-reference-impl", "snip721-roles", + "snip721-roles-impl", "thiserror", ] @@ -2713,7 +2716,6 @@ dependencies = [ "base64 0.21.5", "bincode2", "cosmwasm-schema 1.5.0", - "dao-snip721-extensions", "primitive-types", "schemars 0.8.16", "secret-cosmwasm-std", @@ -2741,10 +2743,27 @@ dependencies = [ "secret-utils 0.13.4", "serde", "shade-protocol", - "snip721-reference-impl", + "snip721-roles-impl", "thiserror", ] +[[package]] +name = "snip721-roles-impl" +version = "1.0.0" +dependencies = [ + "base64 0.21.5", + "bincode2", + "cosmwasm-schema 1.1.11", + "dao-snip721-extensions", + "primitive-types", + "schemars 0.8.16", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", + "secret-toolkit", + "serde", + "shade-protocol", +] + [[package]] name = "spki" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index 947d4ae..5647795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,6 +144,7 @@ serde = { version = "1.0.158", default-features = false, features = ["derive"] } thiserror = { version = "1.0.21" ,default-features = false} cosmwasm-schema = { git = "https://github.com/scrtlabs/cosmwasm/", branch = "secret",default-features = false } snip20-reference-impl = { path = "./contracts/external/snip20-reference-impl/",default-features = false } +snip721-roles-impl = { path = "./contracts/external/snip721-roles-impl/" ,default-features = false} snip721-reference-impl = { path = "./contracts/external/snip721-reference-impl/" ,default-features = false} cosmos-sdk-proto = { version = "0.20.0", default-features = false } shade-protocol = { git = "https://github.com/securesecrets/shade", rev = "lend-v1",features = [ diff --git a/contracts/external/snip721-reference-impl/.circleci/config.yml b/contracts/external/snip721-reference-impl/.circleci/config.yml new file mode 100644 index 0000000..fc7fb91 --- /dev/null +++ b/contracts/external/snip721-reference-impl/.circleci/config.yml @@ -0,0 +1,52 @@ +version: 2.1 + +jobs: + build: + docker: + - image: rust:1.65 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown + - run: + name: Build + command: cargo wasm --locked + - run: + name: Unit tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Integration tests + command: cargo integration-test --locked + - run: + name: Format source code + command: cargo fmt + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in source code and schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + - target/wasm32-unknown-unknown/release/.fingerprint + - target/wasm32-unknown-unknown/release/build + - target/wasm32-unknown-unknown/release/deps + key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} diff --git a/contracts/external/snip721-reference-impl/.github/workflows/Basic.yml b/contracts/external/snip721-reference-impl/.github/workflows/Basic.yml new file mode 100644 index 0000000..c95b5e0 --- /dev/null +++ b/contracts/external/snip721-reference-impl/.github/workflows/Basic.yml @@ -0,0 +1,81 @@ +# Based on https://github.com/actions-rs/example/blob/master/.github/workflows/quickstart.yml + +on: [push, pull_request] + +name: Basic + +jobs: + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.65.0 + target: wasm32-unknown-unknown + override: true + + - name: Run unit tests + uses: actions-rs/cargo@v1 + with: + command: unit-test + args: --locked + env: + RUST_BACKTRACE: 1 + + - name: Compile WASM contract + uses: actions-rs/cargo@v1 + with: + command: wasm + args: --locked + env: + RUSTFLAGS: "-C link-arg=-s" + + - name: Run integration tests + uses: actions-rs/cargo@v1 + with: + command: integration-test + args: --locked + + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.65.0 + override: true + components: rustfmt, clippy + + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + # TODO: we should check + # CHANGES_IN_REPO=$(git status --porcelain) + # after this, but I don't know how + - name: Generate Schema + uses: actions-rs/cargo@v1 + with: + command: schema + args: --locked diff --git a/contracts/external/snip721-reference-impl/Cargo.toml b/contracts/external/snip721-reference-impl/Cargo.toml index 1262898..2b5750c 100644 --- a/contracts/external/snip721-reference-impl/Cargo.toml +++ b/contracts/external/snip721-reference-impl/Cargo.toml @@ -21,15 +21,14 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cosmwasm-std = {workspace=true } -secret-toolkit = {workspace=true } -cosmwasm-storage = {workspace=true } -schemars = {workspace=true } -serde = {workspace=true } +cosmwasm-std = { package = "secret-cosmwasm-std", version = "1.1.11" } +secret-toolkit = { version = "0.10.0", default-features = false, features = ["storage", "serialization", "utils", "permit", "viewing-key", "crypto"] } +cosmwasm-storage = { package = "secret-cosmwasm-storage", version = "1.1.10" } +schemars = "0.8.12" +serde = { version = "1.0.190", default-features = false, features = ["derive"] } bincode2 = "2.0.1" base64 = "0.21.2" primitive-types = { version = "0.12.2", default-features = false } -dao-snip721-extensions ={ workspace = true } [dev-dependencies] diff --git a/contracts/external/snip721-reference-impl/LICENSE.md b/contracts/external/snip721-reference-impl/LICENSE.md new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/contracts/external/snip721-reference-impl/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/contracts/external/snip721-reference-impl/NOTICE.md b/contracts/external/snip721-reference-impl/NOTICE.md new file mode 100644 index 0000000..be103bb --- /dev/null +++ b/contracts/external/snip721-reference-impl/NOTICE.md @@ -0,0 +1,10 @@ +Copyright 2020 Bill Wincer + +Licensed under the GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007; +you may not use these files except in compliance with the [License](https://github.com/baedrik/snip721-reference-impl/blob/master/LICENSE.md). + +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/external/snip721-reference-impl/README.md b/contracts/external/snip721-reference-impl/README.md new file mode 100755 index 0000000..55abaf5 --- /dev/null +++ b/contracts/external/snip721-reference-impl/README.md @@ -0,0 +1,2781 @@ +# SNIP-721 Reference Implementation +***NOTE*** +I'm making the code available early for people who are interested, but I still need to write the specification for SNIP-723 (which includes some miscellaneous improvements like a BatchNftDossier query, a NumTokensOfOwner query to retrieve the count of tokens owned by one address and in which the querier has permission know the tokens' ownership, and adding a token's unwrapped status to the NftDossier response). I also still need to update this README with documentation of the additions. + + + +This is a reference implementation of the [SNIP-721 specification](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-721.md) and [SNIP-722 specification](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md). It not only implements the base requirements of SNIP-721 and SNIP-722, but it also includes additional functionality that may be helpful for many use cases. As SNIP-721 is a superset of the [CW-721 specification](https://github.com/CosmWasm/cw-nfts/tree/main/packages/cw721), this implementation is CW-721 compliant; however, because CW-721 does not support privacy, a number of the CW-721-compliant functions may not return all the information a CW-721 implementation would. For example, the [OwnerOf](#ownerof) query will not display the approvals for a token unless the token owner has supplied his address and viewing key. In order to strive for CW-721 compliance, a number of queries that require authentication use optional parameters that the CW-721 counterpart does not have. If the optional authentication parameters are not supplied, the responses will only display information that the token owner has made public. + +### Terms +- __Message__ - This is an on-chain interface. It is triggered by sending a transaction, and receiving an on-chain response which is read by the client. Messages are authenticated both by the blockchain, and by the secret enclave. +- __Query__ - This is an off-chain interface. Queries are done by returning data that a node has locally, and are not public. Query responses are returned immediately, and do not have to wait for blocks. +- __Cosmos Message Sender__ - The account that is found under the `sender` field in a standard Cosmos SDK message. This is also the signer of the message. + +### Padding +Users may want to enforce constant length messages to avoid leaking data. To support this functionality, every message includes an optional `padding` field. This optional `padding` field is ignored during message processing. + +### Requests +Requests should be sent as base64 encoded JSON. Future versions of Secret Network may add support for other formats as well, but at this time we recommend usage of JSON only. For this reason the parameter descriptions specify the JSON type which must be used. In addition, request parameters will include in parentheses a CosmWasm (or other) underlying type that this value must conform to. E.g. a recipient address is sent as a string, but must also be parsed to a bech32 address. + +### Responses +Message responses will be JSON encoded in the `data` field of the Cosmos response, rather than in the `logs`, except in the case of MintNft, BatchMintNft, and MintNftClones messages, where the token ID(s) will be returned in both the `data` and `logs` fields. This is because minting may frequently be done by a contract, and `data` fields of responses from callback messages do not get forwarded to the sender of the initial message. + +* [Instantiating The Token Contract](#Instantiating-The-Token-Contract) +* Messages + * [MintNft](#MintNft) + * [BatchMintNft](#BatchMintNft) + * [MintNftClones](#MintNftClones) + * [SetMetadata](#setmetadata) + * [SetRoyaltyInfo](#setroyaltyinfo) + * [Reveal](#reveal) + * [MakeOwnershipPrivate](#MakeOwnershipPrivate) + * [SetGlobalApproval](#setglobal) + * [SetWhitelistedApproval](#setwhitelisted) + * [Approve](#Approve) + * [Revoke](#Revoke) + * [ApproveAll](#ApproveAll) + * [RevokeAll](#RevokeAll) + * [TransferNft](#TransferNft) + * [BatchTransferNft](#BatchTransferNft) + * [SendNft](#sendnft) + * [BatchSendNft](#batchsend) + * [BurnNft](#BurnNft) + * [BatchBurnNft](#BatchBurnNft) + * [CreateViewingKey](#CreateViewingKey) + * [SetViewingKey](#SetViewingKey) + * [AddMinters](#AddMinters) + * [RemoveMinters](#RemoveMinters) + * [SetMinters](#SetMinters) + * [SetContractStatus](#SetContractStatus) + * [ChangeAdmin](#ChangeAdmin) + * [RegisterReceiveNft](#registerreceive) + * [RevokePermit](#RevokePermit) +* Queries + * [ContractInfo](#ContractInfo) + * [ContractConfig](#ContractConfig) + * [Minters](#Minters) + * [RegisteredCodeHash](#RegisteredCodeHash) + * [NumTokens](#NumTokens) + * [AllTokens](#AllTokens) + * [IsUnwrapped](#IsUnwrapped) + * [IsTransferable](#istransferable) + * [OwnerOf](#ownerof) + * [NftInfo](#nftinfo) + * [AllNftInfo](#AllNftInfo) + * [PrivateMetadata](#PrivateMetadata) + * [NftDossier](#nftdossier) + * [RoyaltyInfo](#royaltyquery) + * [TokenApprovals](#TokenApprovals) + * [ApprovedForAll](#ApprovedForAll) + * [InventoryApprovals](#inventoryapprovals) + * [Tokens](#Tokens) + * [VerifyTransferApproval](#verifyapproval) + * [ImplementsTokenSubtype](#ImplementsTokenSubtype) + * [ImplementsNonTransferableTokens](#implementsnontransferabletokens) + * [TransactionHistory](#TransactionHistory) + * [WithPermit](#WithPermit) +* [Receiver Interface](#receiver) + * [ReceiveNft](#receivenft) + * [BatchReceiveNft](#batchreceivenft) + +# Instantiating The Token Contract +##### Request +``` +{ + “name”: “name_of_the_token”, + “symbol”: “token_symbol”, + “admin”: “optional_admin_address”, + “entropy”: “string_used_as_entropy_when_generating_random_viewing_keys”, + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + “config”: { + “public_token_supply”: true | false, + “public_owner”: true | false, + “enable_sealed_metadata”: true | false, + “unwrapped_metadata_is_private”: true | false, + “minter_may_update_metadata”: true | false, + “owner_may_update_metadata”: true | false, + “enable_burn”: true | false + }, + “post_init_callback”: { + “msg”: “base64_encoded_Binary_representing_the_msg_to_perform_after_initialization”, + “contract_address”: “address_of_the_contract_being_called_after_initialization”, + “code_hash”: “code_hash_of_the_contract_being_called_after_initialization”, + “send”: [ + { + “denom”: “denom_string_for_native_coin_being_sent_with_this_message”, + “amount”: “amount_of_native_coin_being_sent” + }, + { + "...": "..." + } + ] + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------------------|--------------------------------------------------------|---------------------------------------------------------------------|----------|--------------------| +| name | string | Name of the token contract | no | | +| symbol | string | Token contract symbol | no | | +| admin | string (Addr) | Address to be given admin authority | yes | env.message.sender | +| entropy | string | String used as entropy when generating random viewing keys | no | | +| royalty_info | [RoyaltyInfo (see below)](#royaltyinfo) | Default RoyaltyInfo for the contract | yes | nothing | +| config | [Config (see below)](#config) | Privacy configuration for the contract | yes | defined below | +| post_init_callback | [PostInitCallback (see below)](#postinitcallback) | Information used to perform a callback message after initialization | yes | nothing | + +The contract's default RoyaltyInfo is the RoyaltyInfo that will be assigned to any token that is minted without explicitly defining its own RoyaltyInfo. It should be noted that default RoyaltyInfo only applies to new tokens minted while the default is in effect, and will not alter the royalties for any existing NFTs. This is because a token creator should not be able to sell a token with only 1% advertised royalty, and then change it to 100% once it is purchased. If a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token is minted, it will not inherit default royalties because non-transferable tokens can never be transferred as part of a sale, rendering royalties meaningless. + +### RoyaltyInfo +RoyaltyInfo is used to define royalties to be paid when an NFT is sold. This implementation will only display a token's royalty recipient addresses if the querier has permission to transfer the token, and it will only display the contract's default royalty recipient addresses if the querier is an authorized minter. +``` +{ + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty_(optional_in_query_responses)", + "rate": 100, + }, + { + "...": "..." + } + ] +} +``` +| Name | Type | Description | Optional | +|-------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------|----------| +| decimal_places_in_rates | number (u8) | The number of decimal places used for all rates in `royalties` (e.g. 2 decimals for whole percents) | no | +| royalties | array of [Royalty (see below)](#royalty) | List of royalties to be paid upon sale | no | + +### Royalty +Royalty defines a payment address and a royalty rate to be paid when an NFT is sold. This implementation will only display a token's royalty recipient addresses if the querier has permission to transfer the token, and it will only display the contract's default royalty recipient addresses if the querier is an authorized minter. +``` +{ + "recipient": "address_that_should_be_paid_this_royalty_(optional_in_query_responses)", + "rate": 100, +} +``` +| Name | Type | Description | Optional in Messages | Optional in Query Responses | +|-----------|--------------------|------------------------------------------------------------------------------------------------------------------|----------------------|-----------------------------| +| recipient | string (Addr) | The address that should be paid this royalty | no | yes | +| rate | number (u16) | The royalty rate to be paid using the number of decimals specified in the `RoyaltyInfo` containing this `Royalty`| no | no | + +### Config +Config is the privacy configuration for the contract. +* `public_token_supply` - This config value indicates whether the token IDs and the number of tokens controlled by the contract are public. If the token supply is private, only minters can view the token IDs and number of tokens controlled by the contract (default: False) +* `public_owner` - This config value indicates whether token ownership is public or private by default. Regardless of this setting a user has the ability to change whether the ownership of their tokens is public or private (default: False) +* `enable_sealed_metadata` - This config value indicates whether sealed metadata should be enabled. If sealed metadata is enabled, the private metadata of a newly minted token is not viewable by anyone, not even the owner, until the owner calls the [Reveal](#reveal) message. When Reveal is called, the sealed metadata is irreversibly unwrapped and moved to the public metadata (as default). If `unwrapped_metadata_is_private` is set to true, the sealed metadata will remain as private metadata after unwrapping, but the owner (and anyone the owner has whitelisted) will now be able to see it. Anyone will be able to query the token to know whether it has been unwrapped. This simulates buying/selling a wrapped card that no one knows which card it is until it is unwrapped. If sealed metadata is not enabled, all tokens are considered unwrapped when minted (default: False) +* `unwrapped_metadata_is_private` - This config value indicates if the [Reveal](#reveal) message should keep the sealed metadata private after unwrapping. This config value is ignored if sealed metadata is not enabled (default: False) +* `minter_may_update_metadata` - This config value indicates whether a minter is permitted to update a token's metadata (default: True) +* `owner_may_update_metadata` - This config value indicates whether the owner of a token is permitted to update a token's metadata (default: False) +* `enable_burn` - This config value indicates whether burn functionality is enabled. [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable tokens can always be burned even when burning is disabled. This is because an owner must have a way to dispose of an unwanted, non-transferable token (default: False) +``` +{ + “public_token_supply”: true | false, + “public_owner”: true | false, + “enable_sealed_metadata”: true | false, + “unwrapped_metadata_is_private”: true | false, + “minter_may_update_metadata”: true | false, + “owner_may_update_metadata”: true | false, + “enable_burn”: true | false +} +``` +| Name | Type | Optional | Value If Omitted | +|-------------------------------|------|----------|------------------| +| public_token_supply | bool | yes | false | +| public_owner | bool | yes | false | +| enable_sealed_metadata | bool | yes | false | +| unwrapped_metadata_is_private | bool | yes | false | +| minter_may_update_metadata | bool | yes | true | +| owner_may_update_metadata | bool | yes | false | +| enable_burn | bool | yes | false | + +### PostInitCallback +The PostInitCallback object is used to have the token contract execute an optional callback message after the contract has initialized. This can be useful if another contract is instantiating this token contract and needs the token contract to inform the creating contract of the address it has been given. +``` +{ + “msg”: “base64_encoded_Binary_representing_the_msg_to_perform_after_initialization”, + “contract_address”: “address_of_the_contract_being_called_after_initialization”, + “code_hash”: “code_hash_of_the_contract_being_called_after_initialization”, + “send”: [ + { + “denom”: “denom_string_for_native_coin_being_sent_with_this_message”, + “amount”: “amount_of_native_coin_being_sent” + }, + { + "...": "..." + } + ] +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------------------|---------------------------------------|-------------------------------------------------------------------------------------------------------|----------|------------------| +| msg | string (base64 encoded Binary) | Base64 encoded Binary representation of the callback message to perform after contract initialization | no | | +| contract_address | string (Addr) | Address of the contract to call after initialization | no | | +| code_hash | string | A 32-byte hex encoded string, with the code hash of the contract to call after initialization | no | | +| send | array of [Coin (see below)](#coin) | List of native Coin amounts to send with the callback message | no | | + +#### Coin +Coin is the payment to send with the post-init callback message. Although `send` is not an optional field of the [PostInitCallback](#postinitcallback), because it is an array, you can just use `[]` to not send any payment with the callback message +``` +{ + “denom”: “denom_string_for_native_coin_being_sent_with_this_message”, + “amount”: “amount_of_native_coin_being_sent” +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------|------------------|---------------------------------------------------------------------------|----------|------------------| +| denom | string | The denomination of the native Coin (uscrt for SCRT) | no | | +| amount | string (Uint128) | The amount of the native Coin to send with the PostInitCallback message | no | | + +# Messages +## MintNft +MintNft mints a single token. Only an authorized minting address my execute MintNft. + +[SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) adds the ability to optionally mint non-transferable tokens, which are NFTs that can never have a different owner than the address it was minted to. + +##### Request +``` +{ + "mint_nft": { + "token_id": "optional_ID_of_new_token", + "owner": "optional_address_the_new_token_will_be_minted_to", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "serial_number": { + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, + }, + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "transferable": true | false, + "memo": "optional_memo_for_the_mint_tx", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------|----------|----------------------| +| token_id | string | Identifier for the token to be minted | yes | minting order number | +| owner | string (Addr) | Address of the owner of the minted token | yes | env.message.sender | +| public_metadata | [Metadata (see below)](#metadata) | The metadata that is publicly viewable | yes | nothing | +| private_metadata | [Metadata (see below)](#metadata) | The metadata that is viewable only by the token owner and addresses the owner has whitelisted | yes | nothing | +| serial_number | [SerialNumber (see below)](#serialnumber) | The SerialNumber for this token | yes | nothing | +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | RoyaltyInfo for this token | yes | default RoyaltyInfo | +| transferable | bool | True if the minted token should be transferable | yes | true | +| memo | string | `memo` for the mint tx that is only viewable by addresses involved in the mint (minter, owner)| yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +Setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale, so this implementation will not store any RoyaltyInfo for non-transferable tokens. + +##### Response +``` +{ + "mint_nft": { + "token_id": "ID_of_minted_token", + } +} +``` +The ID of the minted token will also be returned in a LogAttribute with the key `minted`. + +### Metadata +This is the metadata for a token that follows CW-721 metadata specification, which is based on ERC721 Metadata JSON Schema. This implementation will throw an error if both `token_uri` and `extension` are provided. +``` +{ + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|-------------------------------------|--------------------------------------------------------------------------------------|----------|----------------------| +| token_uri | string | Uri pointing to off-chain JSON metadata | yes | nothing | +| extension | [Extension (see below)](#extension) | Data structure defining on-chain metadata | yes | nothing | +This implementation will throw an error if both `token_uri` and `extension` are provided. + +### Extension +This is an on-chain metadata extension struct that conforms to the Stashh metadata standard (which in turn implements https://docs.opensea.io/docs/metadata-standards). Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://`. Feel free to add/delete any fields as necessary. + +[SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) adds an optional `token_subtype` field in Extension to be used for Badges/POAPs. This field is intended to be used by applications in order to differentiate NFTs that are used as Badges/POAPs so that they can be displayed as such because they will be used for things like trophies, achievements, proof of attendence, etc... +``` +{ + "image": "optional_image_url", + "image_data": "optional_raw_svg_image_data", + "external_url": "optional_url_to_view_token_on_your_site", + "description": "optional_token_description", + "name": "optional_token_name", + "attributes": [ + { + "display_type": "optional_display_format_for_numerical_traits", + "trait_type": "optional_name_of_the_trait", + "value": "trait value", + "max_value": "optional_max_value_for_numerical_traits" + }, + { + "...": "...", + }, + ], + "background_color": "optional_six-character_hexadecimal_background_color_(without_pre-pended_`#`)", + "animation_url": "optional_url_to_multimedia_file", + "youtube_url": "optional_url_to_a_YouTube_video", + "media": [ + { + "file_type": "optional_file_type", + "extension": "optional_file_extension", + "authentication": { + "key": "optional_decryption_key_or_password", + "user": "optional_username_for_authentication" + }, + "url": "url_pointing_to_the_multimedia_file" + }, + { + "...": "...", + }, + ], + "protected_attributes": [ "list", "of_attributes", "whose_types", "are_public", "but_values", "are_private" ], + "token_subtype": "badge" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------------------|----------------------------------------------|--------------------------------------------------------------------------------------|----------|----------------------| +| image | string | Url to the token's image | yes | nothing | +| image_data | string | Raw SVG image data that should only be used if there is no `image` field | yes | nothing | +| external_url | string | Url to view the token on your site | yes | nothing | +| description | string | Text description of the token | yes | nothing | +| name | string | Name of the token | yes | nothing | +| attributes | array of [Trait (see below)](#trait) | Token's attributes | yes | nothing | +| background_color | string | Background color represented as a six-character hexadecimal without a pre-pended # | yes | nothing | +| animation_url | string | Url to a multimedia file | yes | nothing | +| youtube_url | string | Url to a YouTube video | yes | nothing | +| media | array of [MediaFile (see below)](#mediafile) | List of multimedia files using Stashh specifications | yes | nothing | +| protected_attributes | array of string | List of attributes whose types are public but whose values are private | yes | nothing | +| token_subtype | string | token subtype used to signify what the NFT is used for, such as "badge" | yes | nothing | + +### Trait +Trait describes a token attribute as defined in https://docs.opensea.io/docs/metadata-standards. +``` +{ + "display_type": "optional_display_format_for_numerical_traits", + "trait_type": "optional_name_of_the_trait", + "value": "trait value", + "max_value": "optional_max_value_for_numerical_traits" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------------|--------|--------------------------------------------------------------------------------------|----------|----------------------| +| display_type | string | Display format for numerical traits | yes | nothing | +| trait_type | string | Name of the trait | yes | nothing | +| value | string | Trait value | no | | +| max_value | string | Maximum value for this numerical trait | yes | nothing | + +### MediaFile +MediaFile is the data structure used by Stashh to reference off-chain multimedia files. It allows for hosted files to be encrypted or authenticated with basic authentication, and for the decryption key or username/password to also be included in the on-chain private metadata. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://`. +``` +{ + "file_type": "optional_file_type", + "extension": "optional_file_extension", + "authentication": { + "key": "optional_decryption_key_or_password", + "user": "optional_username_for_authentication" + }, + "url": "url_pointing_to_the_multimedia_file" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------------|-----------------------------------------------|---------------------------------------------------------------------------------------------|----------|----------------------| +| file_type | string | File type. Stashh currently uses: "image", "video", "audio", "text", "font", "application" | yes | nothing | +| extension | string | File extension | yes | nothing | +| authentication | [Authentication (see below)](#authentication) | Credentials or decryption key for a protected file | yes | nothing | +| url | string | Url to the multimedia file | no | | + +### Authentication +Authentication is used to provide the decryption key or username/password for protected files. +``` +{ + "key": "optional_decryption_key_or_password", + "user": "optional_username_for_authentication" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------|--------|--------------------------------------------------------------------------------------|----------|----------------------| +| key | string | Decryption key or password | yes | nothing | +| user | string | Username for basic authentication | yes | nothing | + +### SerialNumber +SerialNumber is used to serialize identical NFTs. +``` +{ + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------------------------|--------------|-----------------------------------------------------------------------------------------------------|----------|----------------------| +| mint_run | number (u32) | The mint run this token was minted in. This represents batches of NFTs released at the same time | yes | nothing | +| serial_number | number (u32) | The serial number of this token | no | | +| quantity_minted_this_run | number (u32) | The number of tokens minted in this mint run. | yes | nothing | + +A mint run is a group of NFTs released at the same time. So, for example, if a creator decided to make 100 copies, they would all be part of mint run number 1. If they sell well and the creator wants to rerelease that NFT, he could make 100 more copies that would all be part of mint run number 2. The combination of mint_run, serial_number, and quantity_minted_this_run is used to indicate, for example, that this token was number 67 of 1000 minted in mint run number 3. + +## BatchMintNft +BatchMintNft mints a list of tokens. Only an authorized minting address my execute BatchMintNft. + +[SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) adds the ability to optionally mint non-transferable tokens, which are NFTs that can never have a different owner than the address it was minted to. + +##### Request +``` +{ + "batch_mint_nft": { + "mints": [ + { + "token_id": "optional_ID_of_new_token", + "owner": "optional_address_the_new_token_will_be_minted_to", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "serial_number": { + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, + }, + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "transferable": true | false, + "memo": "optional_memo_for_the_mint_tx" + }, + { + "...": "..." + } + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|---------------------------------------|------------------------------------------------------------------------|----------|------------------| +| mints | array of [Mint (see below)](#mint) | A list of all the mint operations to perform | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "batch_mint_nft": { + "token_ids": [ + "IDs", "of", "tokens", "that", "were", "minted", "..." + ] + } +} +``` +The IDs of the minted tokens will also be returned in a LogAttribute with the key `minted`. + +### Mint +The Mint object defines the data necessary to mint one token. +``` +{ + "token_id": "optional_ID_of_new_token", + "owner": "optional_address_the_new_token_will_be_minted_to", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "serial_number": { + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, + }, + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "transferable": true | false, + "memo": "optional_memo_for_the_mint_tx" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------------------|-------------------------------------------|------------------------------------------------------------------------------------------------|----------|----------------------| +| token_id | string | Identifier for the token to be minted | yes | minting order number | +| owner | string (Addr) | Address of the owner of the minted token | yes | env.message.sender | +| public_metadata | [Metadata (see above)](#metadata) | The metadata that is publicly viewable | yes | nothing | +| private_metadata | [Metadata (see above)](#metadata) | The metadata that is viewable only by the token owner and addresses the owner has whitelisted | yes | nothing | +| serial_number | [SerialNumber (see above)](#serialnumber) | The SerialNumber for this token | yes | nothing | +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | RoyaltyInfo for this token | yes | default RoyaltyInfo | +| transferable | bool | True if the minted token should be transferable | yes | true | +| memo | string | `memo` for the mint tx that is only viewable by addresses involved in the mint (minter, owner) | yes | nothing | + +Setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale, so this implementation will not store any RoyaltyInfo for non-transferable tokens. + +## MintNftClones +MintNftClones mints copies of an NFT, giving each one a [MintRunInfo](#mintruninfo) that indicates its serial number and the number of identical NFTs minted with it. If the optional `mint_run_id` is provided, the contract will also indicate which mint run these tokens were minted in, where the first use of the `mint_run_id` will be mint run number 1, the second time MintNftClones is called with that `mint_run_id` will be mint run number 2, etc... If no `mint_run_id` is provided, the MintRunInfo will not include a `mint_run`. + +[SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) adds the ability to optionally mint non-transferable tokens, which are NFTs that can never have a different owner than the address it was minted to. + +##### Request +``` +{ + "mint_nft_clones": { + "mint_run_id": "optional_ID_used_to_track_mint_run_numbers_over_multiple_calls", + "quantity": 100, + "owner": "optional_address_the_new_tokens_will_be_minted_to", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "transferable": true | false, + "memo": "optional_memo_for_the_mint_tx", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------------------|-----------------------------------------|----------------------------------------------------------------------------------------------------------|----------|---------------------| +| mint_run_id | string | Identifier used to track the number of mint runs these clones have had over multiple MintNftClones calls | yes | nothing | +| quantity | number (u32) | Number of clones to mint in this run | no | | +| owner | string (Addr) | Address of the owner of the minted tokens | yes | env.message.sender | +| public_metadata | [Metadata (see above)](#metadata) | The metadata that is publicly viewable | yes | nothing | +| private_metadata | [Metadata (see above)](#metadata) | The metadata that is viewable only by the token owner and addresses the owner has whitelisted | yes | nothing | +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | RoyaltyInfo for these tokens | yes | default RoyaltyInfo | +| transferable | bool | True if the minted token should be transferable | yes | true | +| memo | string | `memo` for the mint tx that is only viewable by addresses involved in the mint (minter, owner) | yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +Setting royalties for a non-transferable token has no purpose, because it can never be transferred as part of a sale, so this implementation will not store any RoyaltyInfo for non-transferable tokens. + +##### Response +``` +{ + "mint_nft_clones": { + "first_minted": "token_id_of_the_first_minted_token", + "last_minted": "token_id_of_the_last_minted_token" + } +} +``` +The IDs of the minted tokens will also be returned in LogAttributes with the keys `first_minted` and `last_minted`. Because the token IDs are sequential, the IDs of the other minted tokens are easily inferred. + +## SetMetadata +SetMetadata will set the public and/or private metadata to the corresponding input if the message sender is either the token owner or an approved minter and they have been given this power by the configuration value chosen during instantiation. The private metadata of a [sealed](#enablesealed) token may not be altered until after it has been unwrapped. + +##### Request +``` +{ + "set_metadata": { + "token_id": "ID_of_token_whose_metadata_should_be_updated", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|------------------|--------------------------------------|------------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token whose metadata should be updated | no | | +| public_metadata | [Metadata (see above)](#metadata) | The new public metadata for the token | yes | nothing | +| private_metadata | [Metadata (see above)](#metadata) | The new private metadata for the token | yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_metadata": { + "status": "success" + } +} +``` + +## SetRoyaltyInfo +If a token_id is supplied, SetRoyaltyInfo will update the specified token's RoyaltyInfo to the input. If no RoyaltyInfo is provided, it will delete the RoyaltyInfo and replace it with the contract's default RoyaltyInfo (if there is one). If no token_id is provided, SetRoyaltyInfo will update the contract's default RoyaltyInfo to the input, or delete it if no RoyaltyInfo is provided.
+Only an authorized minter may update the contract's default RoyaltyInfo.
+Only a token's creator may update its RoyaltyInfo, and only if they are also the current owner. + +This implementation will throw an error if trying to set the royalty of a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token, because they can never be transferred as part of a sale. + +##### Request +``` +{ + "set_royalty_info": { + "token_id": "optional_ID_of_token_whose_royalty_info_should_be_updated", + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------------|-----------------------------------------|------------------------------------------------------------------------|----------|------------------| +| token_id | string | Optional ID of the token whose RoyaltyInfo should be updated | yes | see above | +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | The new RoyaltyInfo | yes | see above | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_royalty_info": { + "status": "success" + } +} +``` + +## Reveal +Reveal unwraps the [sealed](#enablesealed) private metadata, irreversibly marking the token as unwrapped. If the `unwrapped_metadata_is_private` [configuration value](#unwrapprivate) is true, the formerly sealed metadata will remain private, otherwise it will be made public. + +##### Request +``` +{ + "reveal": { + "token_id": "ID_of_the_token_to_unwrap", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------|----------------------|------------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token to unwrap | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "reveal": { + "status": "success" + } +} +``` + +## MakeOwnershipPrivate +MakeOwnershipPrivate is used when the token contract was instantiated with the `public_owner` configuration value set to true. It allows an address to make all of its tokens have private ownership by default. The owner may still use [SetGlobalApproval](#setglobal) or [SetWhitelistedApproval](#setwhitelisted) to make ownership public as desired. + +##### Request +``` +{ + "make_ownership_private": { + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------|----------------------|------------------------------------------------------------------------|----------|------------------| +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "make_ownership_private": { + "status": "success" + } +} +``` + +## SetGlobalApproval +The owner of a token can use SetGlobalApproval to make ownership and/or private metadata viewable by everyone. This can be set for a single token or for an owner's entire inventory of tokens by choosing the appropriate [AccessLevel](#accesslevel). SetGlobalApproval can also be used to revoke any global approval previously granted. + +##### Request +``` +{ + "set_global_approval": { + "token_id": "optional_ID_of_the_token_to_grant_or_revoke_approval_on", + "view_owner": "approve_token" | "all" | "revoke_token" | "none", + "view_private_metadata": "approve_token" | "all" | "revoke_token" | "none", + "expires": "never" | {"at_height": 999999} | {"at_time":999999}, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------|----------|------------------| +| token_id | string | If supplying either `approve_token` or `revoke_token` access, the token whose privacy is being set | yes | nothing | +| view_owner | [AccessLevel (see below)](#accesslevel) | Grant or revoke everyone's permission to view the ownership of a token/inventory | yes | nothing | +| view_private_metadata | [AccessLevel (see below)](#accesslevel) | Grant or revoke everyone's permission to view the private metadata of a token/inventory | yes | nothing | +| expires | [Expiration (see below)](#expiration) | Expiration of any approval granted in this message. Can be a blockheight, time, or never | yes | "never" | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_global_approval": { + "status": "success" + } +} +``` +### AccessLevel +AccessLevel determines the type of access being granted or revoked to the specified address in a [SetWhitelistedApproval](#setwhitelisted) message or to everyone in a [SetGlobalApproval](#setglobal) message. Inventory-wide approval and token-specific approval are mutually exclusive levels of access. The levels are: +* `"approve_token"` - grant approval only on the token specified in the message +* `"revoke_token"` - revoke a previous approval on the specified token +* `"all"` - grant approval for all tokens in the message signer's inventory. This approval will also apply to any tokens the signer acquires after granting `all` approval +* `"none"` - revoke any approval (both token and inventory-wide) previously granted to the specified address (or for everyone if using SetGlobalApproval) + +If the message signer grants an address (or everyone in the case of SetGlobalApproval) `all` (inventory-wide) approval, it will remove any individual token approvals previously granted to that address (or granted to everyone in the case of SetGlobalApproval), and grant that address `all` (inventory-wide) approval. If an address (or everyone in the case of SetGlobalApproval) already has `all` approval, and the message signer grants it `approve_token` approval, if the expiration of the new `approve_token` approval is the same as the expiration of the previous `all` approval, it will just leave the `all` approval in place. If the expirations are different, it will grant `approve_token` approval with the specified expiration for the input token, and all other tokens will be changed to `approve_token` approvals with the expiration of the previous `all` approval, and the `all` (inventory-wide) approval will be removed. If the message signer applies `revoke_token` access to an address that currently has inventory-wide approval, it will remove the inventory-wide approval, and create `approve_token` approvals for that address on every token in the signer's inventory EXCEPT the token specified with the `revoke_token` message. In other words, it will only revoke the approval on that single token. + +### Expiration +The Expiration object is used to set an expiration for any approvals granted in the message. Expiration can be set to a specified blockheight, a time in seconds since epoch 01/01/1970, or "never". Values for blockheight and time are specified as a u64. If no expiration is given, it will default to "never". + +One should be aware that the current blockheight and time is not available to a query on Secret Network at this moment, but there are plans to make the BlockInfo available to queries in a future hardfork. To get around this limitation, the contract saves the BlockInfo every time a message is executed, and uses the blockheight and time of the last message execution to check viewing permission expiration during a query. Therefore it is possible that a whitelisted address may be able to view the owner or metadata of a token past its approval expiration if no one executed any contract message since before the expiration. However, because transferring/burning a token is executing a message, it does have the current blockheight and time available and can not occur if transfer approval has expired. + +* `"never"` - the approval will never expire +* `{"at_time": 1700000000}` - the approval will expire 1700000000 seconds after 01/01/1970 (time value is u64) +* `{"at_height": 3000000}` - the approval will expire at blockheight 3000000 (height value is u64) + +## SetWhitelistedApproval +The owner of a token can use SetWhitelistedApproval to grant an address permission to view ownership, view private metadata, and/or to transfer a single token or every token in the owner's inventory. SetWhitelistedApproval can also be used to revoke any approval previously granted to the address. + +##### Request +``` +{ + "set_whitelisted_approval": { + "address": "address_being_granted_or_revoked_approval", + "token_id": "optional_ID_of_the_token_to_grant_or_revoke_approval_on", + "view_owner": "approve_token" | "all" | "revoke_token" | "none", + "view_private_metadata": "approve_token" | "all" | "revoke_token" | "none", + "transfer": "approve_token" | "all" | "revoke_token" | "none", + "expires": "never" | {"at_height": 999999} | {"at_time":999999}, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|--------------------------------------------|----------------------------------------------------------------------------------------------------|----------|------------------| +| address | string (Addr) | Address to grant or revoke approval to/from | no | | +| token_id | string | If supplying either `approve_token` or `revoke_token` access, the token whose privacy is being set | yes | nothing | +| view_owner | [AccessLevel (see above)](#accesslevel) | Grant or revoke the address' permission to view the ownership of a token/inventory | yes | nothing | +| view_private_metadata | [AccessLevel (see above)](#accesslevel) | Grant or revoke the address' permission to view the private metadata of a token/inventory | yes | nothing | +| transfer | [AccessLevel (see above)](#accesslevel) | Grant or revoke the address' permission to transfer a token/inventory | yes | nothing | +| expires | [Expiration (see above)](#expiration) | The expiration of any approval granted in this message. Can be a blockheight, time, or never | yes | "never" | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_whitelisted_approval": { + "status": "success" + } +} +``` + +## Approve +Approve is used to grant an address permission to transfer a single token. This can only be performed by the token's owner or, in compliance with CW-721, an address that has inventory-wide approval to transfer the owner's tokens. Approve is provided to maintain compliance with CW-721, but the owner can use [SetWhitelistedApproval](#setwhitelisted) to accomplish the same thing if specifying a `token_id` and `approve_token` [AccessLevel](#accesslevel) for `transfer`. + +##### Request +``` +{ + "approve": { + "spender": "address_being_granted_approval_to_transfer_the_specified_token", + "token_id": "ID_of_the_token_that_can_now_be_transferred_by_the_spender", + "expires": "never" | {"at_height": 999999} | {"at_time":999999}, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|------------------------------------------|------------------------------------------------------------------------------------------------------|----------|------------------| +| spender | string (Addr) | Address being granted approval to transfer the token | no | | +| token_id | string | ID of the token that the spender can now transfer | no | | +| expires | [Expiration (see above)](#expiration) | The expiration of this token transfer approval. Can be a blockheight, time, or never | yes | "never" | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "approve": { + "status": "success" + } +} +``` + +## Revoke +Revoke is used to revoke from an address the permission to transfer this single token. This can only be performed by the token's owner or, in compliance with CW-721, an address that has inventory-wide approval to transfer the owner's tokens (referred to as an operator later). However, one operator may not revoke transfer permission of even one single token away from another operator. Revoke is provided to maintain compliance with CW-721, but the owner can use [SetWhitelistedApproval](#setwhitelisted) to accomplish the same thing if specifying a `token_id` and `revoke_token` [AccessLevel](#accesslevel) for `transfer`. + +##### Request +``` +{ + "revoke": { + "spender": "address_being_revoked_approval_to_transfer_the_specified_token", + "token_id": "ID_of_the_token_that_can_no_longer_be_transferred_by_the_spender", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|-------------------------|------------------------------------------------------------------------------------------------------|----------|------------------| +| spender | string (Addr) | Address no longer permitted to transfer the token | no | | +| token_id | string | ID of the token that the spender can no longer transfer | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "revoke": { + "status": "success" + } +} +``` + +## ApproveAll +ApproveAll is used to grant an address permission to transfer all the tokens in the message sender's inventory. This will include the ability to transfer any tokens the sender acquires after granting this inventory-wide approval. This also gives the address the ability to grant another address the approval to transfer a single token. ApproveAll is provided to maintain compliance with CW-721, but the message sender can use [SetWhitelistedApproval](#setwhitelisted) to accomplish the same thing by using `all` [AccessLevel](#accesslevel) for `transfer`. + +##### Request +``` +{ + "approve_all": { + "operator": "address_being_granted_inventory-wide_approval_to_transfer_tokens", + "expires": "never" | {"at_height": 999999} | {"at_time":999999}, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|------------------------------------------|------------------------------------------------------------------------------------------------------|----------|------------------| +| operator | string (Addr) | Address being granted approval to transfer all of the message sender's tokens | no | | +| expires | [Expiration (see above)](#expiration) | The expiration of this inventory-wide transfer approval. Can be a blockheight, time, or never | yes | "never" | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "approve_all": { + "status": "success" + } +} +``` + +## RevokeAll +RevokeAll is used to revoke all transfer approvals granted to an address. RevokeAll is provided to maintain compliance with CW-721, but the message sender can use [SetWhitelistedApproval](#setwhitelisted) to accomplish the same thing by using `none` [AccessLevel](#accesslevel) for `transfer`. + +##### Request +``` +{ + "revoke_all": { + "operator": "address_being_revoked_all_approvals_to_transfer_tokens", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------|-------------------------|------------------------------------------------------------------------------------------------------|----------|------------------| +| operator | string (Addr) | Address being revoked all approvals to transfer the message sender's tokens | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "revoke_all": { + "status": "success" + } +} +``` + +## TransferNft +TransferNft is used to transfer ownership of the token to the `recipient` address. This requires a valid `token_id` and the message sender must either be the owner or an address with valid transfer approval. If the `recipient` address is the same as the current owner, the contract will throw an error. If the token is transferred to a new owner, its single-token approvals will be cleared. + +This implementation will throw an error if trying to transfer a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token. + +##### Request +``` +{ + "transfer_nft": { + "recipient": "address_receiving_the_token", + "token_id": "ID_of_the_token_being_transferred", + "memo": "optional_memo_for_the_transfer_tx", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| recipient | string (Addr) | Address receiving the token | no | | +| token_id | string | Identifier of the token to be transferred | no | | +| memo | string | `memo` for the transfer transaction that is only viewable by addresses involved in the transfer (recipient, sender, previous owner) | yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "transfer_nft": { + "status": "success" + } +} +``` + +## BatchTransferNft +BatchTransferNft is used to perform multiple token transfers. The message sender may specify a list of tokens to transfer to one `recipient` address in each [Transfer](#transfer) object, and any `memo` provided will be applied to every token transferred in that one `Transfer` object. The message sender may provide multiple `Transfer` objects to perform transfers to multiple addresses, providing a different `memo` for each address if desired. Each individual transfer of a token will show separately in transaction histories. The message sender must have permission to transfer all the tokens listed (either by being the owner or being granted transfer approval) and every listed `token_id` must be valid. A contract may use the [VerifyTransferApproval](#verifyapproval) query to verify that it has permission to transfer all the tokens. + +If the message sender does not have permission to transfer any one of the listed tokens, the entire message will fail (no tokens will be transferred) and the error will provide the ID of the first token encountered in which the sender does not have the required permission. If any token transfer involves a `recipient` address that is the same as its current owner, the contract will throw an error. Any token that is transferred to a new owner will have its single-token approvals cleared. + +This implementation will throw an error if trying to transfer a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token. + +##### Request +``` +{ + "batch_transfer_nft": { + "transfers": [ + { + "recipient": "address_receiving_the_tokens", + "token_ids": [ + "list", "of", "token", "IDs", "to", "transfer" + ], + "memo": "optional_memo_applied_to_the_transfer_tx_for_every_token_listed_in_this_Transfer_object" + }, + { + "...": "..." + } + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|-----------------------------------------------|------------------------------------------------------------------------------------------------------|----------|------------------| +| transfers | array of [Transfer (see below)](#transfer) | List of `Transfer` objects to process | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "batch_transfer_nft": { + "status": "success" + } +} +``` + +### Transfer +The Transfer object provides a list of tokens to transfer to one `recipient` address, as well as an optional `memo` that would be included with every logged token transfer. +``` +{ + "recipient": "address_receiving_the_tokens", + "token_ids": [ + "list", "of", "token", "IDs", "to", "transfer", "..." + ], + "memo": "optional_memo_applied_to_the_transfer_tx_for_every_token_listed_in_this_Transfer_object" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| recipient | string (Addr) | Address receiving the listed tokens | no | | +| token_ids | array of string | List of token IDs to transfer to the `recipient` | no | | +| memo | string | `memo` for the transfer transactions that is only viewable by addresses involved in the transfer (recipient, sender, previous owner)| yes | nothing | + +## SendNft +SendNft is used to transfer ownership of the token to the `contract` address, and then call the recipient's [BatchReceiveNft](#batchreceivenft) (or [ReceiveNft](#receivenft)) if the recipient contract has registered its receiver interface with the NFT contract or if its [ReceiverInfo](#receiverinfo) is provided. If the recipient contract registered (or if the `ReceiverInfo` indicates) that it implements BatchReceiveNft, a BatchReceiveNft callback will be performed with only the single token ID in the `token_ids` array. + +While SendNft keeps the `contract` field name in order to maintain CW-721 compliance, Secret Network does not have the same limitations as Cosmos, and it is possible to use SendNft to transfer token ownership to a personal address (not a contract) or to a contract that does not implement any [Receiver Interface](#receiver). + +SendNft requires a valid `token_id` and the message sender must either be the owner or an address with valid transfer approval. If the recipient address is the same as the current owner, the contract will throw an error. If the token is transferred to a new owner, its single-token approvals will be cleared. If the BatchReceiveNft (or ReceiveNft) callback fails, the entire transaction will be reverted (even the transfer will not take place). + +This implementation will throw an error if trying to send a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token. + +##### Request +``` +{ + "send_nft": { + "contract": "address_receiving_the_token", + "receiver_info": { + "recipient_code_hash": "code_hash_of_the_recipient_contract", + "also_implements_batch_receive_nft": true | false, + }, + "token_id": "ID_of_the_token_being_transferred", + "msg": "optional_base64_encoded_Binary_message_sent_with_the_BatchReceiveNft_callback", + "memo": "optional_memo_for_the_transfer_tx", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------|----------|------------------| +| contract | string (Addr) | Address receiving the token | no | | +| receiver_info | [ReceiverInfo (see below)](#receiverinfo) | Code hash and BatchReceiveNft implementation status of the recipient contract | yes | nothing | +| token_id | string | Identifier of the token to be transferred | no | | +| msg | string (base64 encoded Binary) | `msg` included when calling the recipient contract's BatchReceiveNft (or ReceiveNft) | yes | nothing | +| memo | string | `memo` for the tx that is only viewable by addresses involved (recipient, sender, previous owner) | yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "send_nft": { + "status": "success" + } +} +``` +### ReceiverInfo +The optional ReceiverInfo object may be used to provide the code hash of the contract receiving tokens from either [SendNft](#sendnft) or [BatchSendNft](#batchsend). It may also optionally indicate whether the recipient contract implements [BatchReceiveNft](#batchreceivenft) in addition to [ReceiveNft](#receivenft). If the `also_implements_batch_receive_nft` field is not provided, it defaults to `false`. +``` +{ + "recipient_code_hash": "code_hash_of_the_recipient_contract", + "also_implements_batch_receive_nft": true | false, +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------------------|--------|------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| recipient_code_hash | string | Code hash of the recipient contract | no | | +| also_implements_batch_receive_nft | bool | True if the recipient contract implements [BatchReceiveNft](#batchreceivenft) in addition to [ReceiveNft](#receivenft) | yes | false | + +## BatchSendNft +BatchSendNft is used to perform multiple token transfers, and then call the recipient contracts' [BatchReceiveNft](#batchreceivenft) (or [ReceiveNft](#receivenft)) if they have registered their receiver interface with the NFT contract or if their [ReceiverInfo](#receiverinfo) is provided. The message sender may specify a list of tokens to send to one recipient address in each [Send](#send) object, and any `memo` or `msg` provided will be applied to every token transferred in that one `Send` object. If the list of transferred tokens belonged to multiple previous owners, a separate BatchReceiveNft callback will be performed for each of the previous owners. If the contract only implements ReceiveNft, one ReceiveNft will be performed for every sent token. Therefore it is highly recommended to implement BatchReceiveNft if there is the possibility of being sent multiple tokens at one time. This will significantly reduce gas costs. + +The message sender may provide multiple [Send](#send) objects to perform sends to multiple addresses, providing a different `memo` and `msg` for each address if desired. Each individual transfer of a token will show separately in transaction histories. The message sender must have permission to transfer all the tokens listed (either by being the owner or being granted transfer approval) and every token ID must be valid. A contract may use the [VerifyTransferApproval](#verifyapproval) query to verify that it has permission to transfer all the tokens. If the message sender does not have permission to transfer any one of the listed tokens, the entire message will fail (no tokens will be transferred) and the error will provide the ID of the first token encountered in which the sender does not have the required permission. If any token transfer involves a recipient address that is the same as its current owner, the contract will throw an error. Any token that is transferred to a new owner will have its single-token approvals cleared. +If any BatchReceiveNft (or ReceiveNft) callback fails, the entire transaction will be reverted (even the transfers will not take place). + +This implementation will throw an error if trying to send a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token. + +##### Request +``` +{ + "batch_send_nft": { + "sends": [ + { + "contract": "address_receiving_the_tokens", + "receiver_info": { + "recipient_code_hash": "code_hash_of_the_recipient_contract", + "also_implements_batch_receive_nft": true | false, + }, + "token_ids": [ + "list", "of", "token", "IDs", "to", "transfer", "..." + ], + "msg": "optional_base64_encoded_Binary_message_sent_with_every_BatchReceiveNft_callback_made_for_this_one_Send_object", + "memo": "optional_memo_applied_to_the_transfer_tx_for_every_token_listed_in_this_Send_object" + }, + { + "...": "..." + } + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|---------------------------------------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| sends | array of [Send (see below)](#send) | List of `Send` objects to process | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "batch_send_nft": { + "status": "success" + } +} +``` + +### Send +The Send object provides a list of tokens to transfer to one recipient address, optionally provides the [ReceiverInfo](#receiverinfo) of the recipient contract, as well as an optional `memo` that would be included with every logged token transfer, and an optional `msg` that would be included with every [BatchReceiveNft](#batchreceivenft) or [ReceiveNft](#receivenft) callback made as a result of this Send object. While Send keeps the `contract` field name in order be consistent with CW-721 specification, Secret Network does not have the same limitations as Cosmos, and it is possible to use [BatchSendNft](#batchsend) to transfer token ownership to a personal address (not a contract) or to a contract that does not implement any [Receiver Interface](#receiver). +``` +{ + "contract": "address_receiving_the_tokens", + "receiver_info": { + "recipient_code_hash": "code_hash_of_the_recipient_contract", + "also_implements_batch_receive_nft": true | false, + }, + "token_ids": [ + "list", "of", "token", "IDs", "to", "transfer", "..." + ], + "msg": "optional_base64_encoded_Binary_message_sent_with_every_BatchReceiveNft_callback_made_for_this_one_Send_object", + "memo": "optional_memo_applied_to_the_transfer_tx_for_every_token_listed_in_this_Send_object" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------|----------|------------------| +| contract | string (Addr) | Address receiving the token | no | | +| receiver_info | [ReceiverInfo (see above)](#receiverinfo) | Code hash and BatchReceiveNft implementation status of the recipient contract | yes | nothing | +| token_ids | array of string | List of token IDs to send to the recipient | no | | +| msg | string (base64 encoded Binary) | `msg` included when calling the recipient contract's BatchReceiveNft (or ReceiveNft) | yes | nothing | +| memo | string | `memo` for the tx that is only viewable by addresses involved (recipient, sender, previous owner) | yes | nothing | + +## BurnNft +BurnNft is used to burn a single token, providing an optional `memo` to include in the burn's transaction history if desired. If the contract has not enabled burn functionality using the init configuration `enable_burn`, BurnNft will result in an error, unless the token being burned is a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token. This is because an owner should always be able to dispose of an unwanted, non-transferable token. Only the token owner and anyone else with valid transfer approval may burn this token. + +##### Request +``` +{ + "burn_nft": { + "token_id": "ID_of_the_token_to_burn", + "memo": "optional_memo_for_the_burn_tx", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| token_id | string | Identifier of the token to burn | no | | +| memo | string | `memo` for the burn tx that is only viewable by addresses involved in the burn (message sender and previous owner if different) | yes | nothing | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "burn_nft": { + "status": "success" + } +} +``` + +## BatchBurnNft +BatchBurnNft is used to burn multiple tokens. The message sender may specify a list of tokens to burn in each [Burn](#burn) object, and any `memo` provided will be applied to every token burned in that one `Burn` object. The message sender will usually list every token to be burned in one `Burn` object, but if a different `memo` is needed for different tokens being burned, multiple `Burn` objects may be listed. Each individual burning of a token will show separately in transaction histories. The message sender must have permission to transfer/burn all the tokens listed (either by being the owner or being granted transfer approval). A contract may use the [VerifyTransferApproval](#verifyapproval) query to verify that it has permission to transfer/burn all the tokens. If the message sender does not have permission to transfer/burn any one of the listed tokens, the entire message will fail (no tokens will be burned) and the error will provide the ID of the first token encountered in which the sender does not have the required permission. + +A [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token can always be burned even if burn functionality has been disabled using the init configuration. This is because an owner should always be able to dispose of an unwanted, non-transferable token. + +##### Request +``` +{ + "batch_burn_nft": { + "burns": [ + { + "token_ids": [ + "list", "of", "token", "IDs", "to", "burn" + ], + "memo": "optional_memo_applied_to_the_burn_tx_for_every_token_listed_in_this_Burn_object" + }, + { + "...": "..." + } + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|---------------------------------------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| burns | array of [Burn (see below)](#burn) | List of `Burn` objects to process | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "batch_burn_nft": { + "status": "success" + } +} +``` + +### Burn +The Burn object provides a list of tokens to burn, as well as an optional `memo` that would be included with every token burn transaction history. +``` +{ + "token_ids": [ + "list", "of", "token", "IDs", "to", "burn", "..." + ], + "memo": "optional_memo_applied_to_the_burn_tx_for_every_token_listed_in_this_Burn_object" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|-----------------|----------------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| token_ids | array of string | List of token IDs to burn | no | | +| memo | string | `memo` for the burn txs that is only viewable by addresses involved in the burn (message sender and previous owner if different) | yes | nothing | + +## CreateViewingKey +CreateViewingKey is used to generate a random viewing key to be used to authenticate account-specific queries. The `entropy` field is a client-supplied string used as part of the entropy supplied to the rng that creates the viewing key. + +##### Request +``` +{ + "create_viewing_key": { + "entropy": "string_used_as_part_of_the_entropy_supplied_to_the_rng", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| entropy | string | String used as part of the entropy supplied to the rng that generates the random viewing key | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "viewing_key": { + "key": "the_created_viewing_key" + } +} +``` + +## SetViewingKey +SetViewingKey is used to set the viewing key to a predefined string. It will replace any key that currently exists. It would be best for users to call CreateViewingKey to ensure a strong key, but this function is provided so that contracts can also utilize viewing keys. + +##### Request +``` +{ + "set_viewing_key": { + "key": "the_new_viewing_key", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| key | string | The new viewing key for the message sender | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "viewing_key": { + "key": "the_message_sender's_viewing_key" + } +} +``` + +## AddMinters +AddMinters will add the provided addresses to the list of authorized minters. This can only be called by the admin address. + +##### Request +``` +{ + "add_minters": { + "minters": [ + "list", "of", "addresses", "to", "add", "to", "the", "list", "of", "minters", "..." + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|-----------------------------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| minters | array of string (Addr) | The list of addresses to add to the list of authorized minters | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "add_minters": { + "status": "success" + } +} +``` + +## RemoveMinters +RemoveMinters will remove the provided addresses from the list of authorized minters. This can only be called by the admin address. + +##### Request +``` +{ + "remove_minters": { + "minters": [ + "list", "of", "addresses", "to", "remove", "from", "the", "list", "of", "minters", "..." + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|-----------------------------|--------------------------------------------------------------------------------------------------------------------|----------|------------------| +| minters | array of string (Addr) | The list of addresses to remove from the list of authorized minters | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "remove_minters": { + "status": "success" + } +} +``` + +## SetMinters +SetMinters will precisely define the list of authorized minters. This can only be called by the admin address. + +##### Request +``` +{ + "set_minters": { + "minters": [ + "list", "of", "addresses", "that", "have", "minting", "authority", "..." + ], + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|-----------------------------|---------------------------------------------------------------------------------------------|----------|------------------| +| minters | array of string (Addr) | The list of addresses to are allowed to mint | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_minters": { + "status": "success" + } +} +``` + +## SetContractStatus +SetContractStatus allows the contract admin to define which messages the contract will execute. This can only be called by the admin address. + +##### Request +``` +{ + "set_contract_status": { + "level": "normal" | "stop_transactions" | "stop_all", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|--------------------------------------------------|---------------------------------------------------------------------------------------------|----------|------------------| +| level | [ContractStatus (see below)](#contractstatus) | The level that defines which messages the contract will execute | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "set_contract_status": { + "status": "success" + } +} +``` +### ContractStatus +ContractStatus indicates which messages the contract will execute. The possible values are: +* `"normal"` - the contract will execute all messages +* `"stop_transactions"` - the contract will not allow any minting, burning, sending, or transferring of tokens +* `"stop_all"` - the contract will only execute a SetContractStatus message + +## ChangeAdmin +ChangeAdmin will allow the current admin to transfer admin privileges to another address (which will be the only admin address). This can only be called by the current admin address. + +##### Request +``` +{ + "change_admin": { + "address": "address_of_the_new_contract_admin", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|---------|--------------------|---------------------------------------------------------------------------------------------|----------|------------------| +| address | string (Addr) | Address of the new contract admin | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "change_admin": { + "status": "success" + } +} +``` + +## RegisterReceiveNft +A contract will use RegisterReceiveNft to notify the NFT contract that it implements ReceiveNft and possibly also BatchReceiveNft [(see below)](#receiver). This enables the NFT contract to call the registered contract whenever it is Sent a token (or tokens). In order to comply with CW-721, ReceiveNft only informs the recipient contract that it has been sent a single token, and it only informs the recipient contract who the token's previous owner was, not who sent the token (which may be different addresses) despite calling the previous owner `sender` ([see below](#cwsender)). BatchReceiveNft, on the other hand, can be used to inform a contract that it was sent multiple tokens, and notifies the recipient of both, the token's previous owner and the sender. If a contract implements BatchReceiveNft, the NFT contract will always call BatchReceiveNft even if there is only one token being sent, in which case the `token_ids` array will only have one element. + +##### Request +``` +{ + "register_receive_nft": { + "code_hash": "code_hash_of_the_contract_implementing_a_receiver_interface", + "also_implements_batch_receive_nft": true | false, + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------------------|--------|----------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| code_hash | string | A 32-byte hex encoded string, with the code hash of the message sender, which is a contract that implements a receiver | no | | +| also_implements_batch_receive_nft | bool | true if the message sender contract also implements BatchReceiveNft so it can be informed that it was sent a list of tokens| yes | false | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "register_receive_nft": { + "status": "success" + } +} +``` + +## RevokePermit +RevokePermit allows a user to disable the use of a permit for authenticated queries. + +##### Request +``` +{ + "revoke_permit": { + "permit_name": "name_of_the_permit_that_is_no_longer_valid", + "padding": "optional_ignored_string_that_can_be_used_to_maintain_constant_message_length" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------------------------|--------|---------------------------------------------------------------------------|----------|------------------| +| permit_name | string | name of the permit that is no longer valid | no | | +| padding | string | An ignored string that can be used to maintain constant message length | yes | nothing | + +##### Response +``` +{ + "revoke_permit": { + "status": "success" + } +} +``` + +# Queries +Queries are off-chain requests, that are not cryptographically validated; therefore, this contract utilizes viewing keys to authenticate address-specific queries. It makes viewing key validation resource intensive in order to combat offline brute-force attempts to guess viewing keys. Also, even if a user has not set a viewing key, it will perform the same resource intensive processing to prevent an attacker from knowing that a key has not been set and provides the same error response whether there is no viewing key or if the input key does not match. + +Any query that inquires about a specific token will return an error if the input token ID does not exist. If the token supply is public, the error will indicate that the token does not exist. If the token supply is private, the query will return the same error response whether the token does not exist or the querier does not have permission to view the requested information. + +One should be aware that the current blockheight and time is not available to a query on Secret Network at this moment, but there are plans to make the BlockInfo available to queries in a future hardfork. To get around this limitation, the contract saves the BlockInfo every time a message is executed, and uses the blockheight and time of the last message execution to check viewing approval expiration during a query. Therefore it is possible that a whitelisted address may be able to view the owner or metadata of a token past its approval expiration if no one executed any contract message since before the expiration. However, because transferring/burning a token is executing a message, it does have the current blockheight and time available and can enforce exact expiration. + +## ContractInfo +ContractInfo returns the contract's name and symbol. This query is not authenticated. + +##### Request +``` +{ + "contract_info": {} +} +``` +##### Response +``` +{ + "contract_info": { + "name": "contract_name", + "symbol": "contract_symbol" + } +} +``` + +## ContractConfig +ContractConfig returns the configuration values that were selected when the contract was instantiated. See [Config](#config) for an explanation of the configuration options. ContractConfig also returns whether non-transferable tokens and token subtypes are implemented. This query is not authenticated. + +##### Request +``` +{ + "contract_config": {} +} +``` +##### Response +``` +{ + "contract_config": { + “token_supply_is_public”: true | false, + “owner_is_public”: true | false, + “sealed_metadata_is_enabled”: true | false, + “unwrapped_metadata_is_private”: true | false, + “minter_may_update_metadata”: true | false, + “owner_may_update_metadata”: true | false, + “burn_is_enabled”: true | false, + "implements_non_transferable_tokens": true | false, + "implements_token_subtype": true | false + } +} +``` +| Name | Type | Description | Optional | +|------------------------------------|------|--------------------------------------------------------------------------------------------|----------| +| token_supply_is_public | bool | True if token IDs and the number of tokens controlled by the contract are public | no | +| owner_is_public | bool | True if newly minted coins have public ownership as default | no | +| sealed_metadata_is_enabled | bool | True if newly minted coins have sealed metadata | no | +| unwrapped_metadata_is_private | bool | True if sealed metadata remains private after unwrapping | no | +| minter_may_update_metadata | bool | True if authorized minters may alter a token's metadata | no | +| owner_may_update_metadata | bool | True if a token owner may alter its metadata | no | +| burn_is_enabled | bool | True if burn functionality is enabled | no | +| implements_non_transferable_tokens | bool | True if the contract implements non-transferable tokens | no | +| implements_token_subtype | bool | True if the contract implements token subtypes | no | + +## Minters +Minters returns the list of addresses that are authorized to mint tokens. This query is not authenticated. + +##### Request +``` +{ + "minters": {} +} +``` +##### Response +``` +{ + "minters": { + “minters”: [ + "list", "of", "authorized", "minters", "..." + ] + } +} +``` +| Name | Type | Description | Optional | +|---------|-----------------------------|------------------------------------------|----------| +| minters | array of string (Addr) | List of addresses with minting authority | no | + +## RegisteredCodeHash +RegisteredCodeHash will display the code hash of the specified contract if it has registered its [receiver interface](#receiver) and will indicate whether the contract implements [BatchReceiveNft](#batchreceivenft). + +##### Request +``` +{ + "registered_code_hash": { + "contract": "address_of_the_contract_whose_registration_is_being_queried" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------|--------------------|-----------------------------------------------------------------------|----------|------------------| +| contract | string (Addr) | The address of the contract whose registration info is being queried | no | | + +##### Response +``` +{ + "registered_code_hash": { + "code_hash": "code_hash_of_the_registered_contract", + "also_implements_batch_receive_nft": true | false + } +} +``` +| Name | Type | Description | Optional | +|------------------------------------|--------|------------------------------------------------------------------------------|----------| +| code_hash | string | A 32-byte hex encoded string, with the code hash of the registered contract | yes | +| also_implements_batch_receive_nft | bool | True if the registered contract also implements BatchReceiveNft | no | + +## NumTokens +NumTokens returns the number of tokens controlled by the contract. If the contract's token supply is private, only an authenticated minter's address will be allowed to perform this query. + +##### Request +``` +{ + "num_tokens": { + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + } + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------|---------------------------------------|---------------------------------------------------------------------|----------|------------------| +| viewer | [ViewerInfo (see below)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | + +##### Response +``` +{ + "num_tokens": { + "count": 99999 + } +} +``` +| Name | Type | Description | Optional | +|---------|--------------|----------------------------------------------|----------| +| count | number (u32) | Number of tokens controlled by this contract | no | + +### ViewerInfo +The ViewerInfo object provides the address and viewing key of the querier. It is optionally provided in queries where public responses and address-specific responses will differ. +``` +{ + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------------------|-----------------------------------------------------------------------------------------------------------------------|----------|------------------| +| address | string (Addr) | Address performing the query | no | | +| viewing_key | string | The querying address' viewing key | no | | + +## AllTokens +AllTokens returns an optionally paginated list of all the token IDs controlled by the contract. If the contract's token supply is private, only an authenticated minter's address will be allowed to perform this query. When paginating, supply the last token ID received in a response as the `start_after` token ID of the next query to continue listing where the previous query stopped. + +##### Request +``` +{ + "all_tokens": { + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + "start_after": "optionally_display_only_token_ids_that_come_after_this_one_in_the_list", + "limit": 10 + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|---------------------------------------|------------------------------------------------------------------------------------------|----------|------------------| +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | +| start_after | string | Results will only list token IDs that come after this token ID in the list | yes | nothing | +| limit | number (u32) | Number of token IDs to return | yes | 300 | + +##### Response +``` +{ + "token_list": { + "tokens": [ + "list", "of", "token", "IDs", "controlled", "by", "the", "contract", "..." + ] + } +} +``` +| Name | Type | Description | Optional | +|---------|-----------------|----------------------------------------------------------------------|----------| +| tokens | array of string | A list of token IDs controlled by this contract | no | + +## IsUnwrapped +IsUnwrapped indicates whether the token has been unwrapped. If [sealed metadata](#enablesealed) is not enabled, all tokens are considered to be unwrapped. This query is not authenticated. + +##### Request +``` +{ + "is_unwrapped": { + "token_id": "ID_of_the_token_whose_unwrapped_status_is_being_queried" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------|------------------------------------------------------------------------------------------|----------|------------------| +| token_id | string | The ID of the token whose unwrapped status is being queried | no | | + +##### Response +``` +{ + "is_unwrapped": { + "token_is_unwrapped": true | false + } +} +``` +| Name | Type | Description | Optional | +|---------------------|------|---------------------------------------------------------------------------------------|----------| +| token_is_unwrapped | bool | True if the token is unwrapped (or [sealed metadata](#enablesealed) is not enabled) | no | + +## IsTransferable +IsTransferable is a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) query that indicates whether the token is transferable. This query is not authenticated. + +##### Request +``` +{ + "is_transferable": { + "token_id": "ID_of_the_token_whose_transferability_is_being_queried" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------|------------------------------------------------------------------------------------------|----------|------------------| +| token_id | string | The ID of the token whose transferability is being queried | no | | + +##### Response +``` +{ + "is_transferable": { + "token_is_transferable": true | false + } +} +``` +| Name | Type | Description | Optional | +|------------------------|------|-------------------------------------------------------------------------|----------| +| token_is_transferable | bool | True if the token is transferable | no | + +## OwnerOf +OwnerOf returns the owner of the specified token if the querier is the owner or has been granted permission to view the owner. If the querier is the owner, OwnerOf will also display all the addresses that have been given transfer permission. The transfer approval list is provided as part of CW-721 compliance; however, the token owner is advised to use [NftDossier](#nftdossier) for a more complete list that includes view_owner and view_private_metadata approvals (which CW-721 is not capable of keeping private). If no [viewer](#viewerinfo) is provided, OwnerOf will only display the owner if ownership is public for this token. + +##### Request +``` +{ + "owner_of": { + "token_id": "ID_of_the_token_being_queried", + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | +| include_expired | bool | True if expired transfer approvals should be included in the response | yes | false | + +##### Response +``` +{ + "owner_of": { + "owner": "address_of_the_token_owner", + "approvals": [ + { + "spender": "address_with_transfer_approval", + "expires": "never" | {"at_height": 999999} | {"at_time":999999} + }, + { + "...": "..." + } + ] + } +} +``` +| Name | Type | Description | Optional | +|-----------|------------------------------------------------------|----------------------------------------------------------|----------| +| owner | string (Addr) | Address of the token's owner | no | +| approvals | array of [Cw721Approval (see below)](#cw721approval) | List of approvals to transfer this token | no | + +### Cw721Approval +The Cw721Approval object is used to display CW-721-style approvals which are limited to only permission to transfer, as CW-721 does not enable ownership or metadata privacy. +``` +{ + "spender": "address_with_transfer_approval", + "expires": "never" | {"at_height": 999999} | {"at_time":999999} +} +``` +| Name | Type | Description | Optional | +|---------|---------------------------------------|---------------------------------------------------------------------------------|----------| +| spender | string (Addr) | Address whitelisted to transfer a token | no | +| expires | [Expiration (see above)](#expiration) | The expiration of this transfer approval. Can be a blockheight, time, or never | no | + +## NftInfo +NftInfo returns the public metadata of a token. It follows CW-721 specification, which is based on ERC-721 Metadata JSON Schema. At most, one of the fields `token_uri` OR `extension` will be defined. + +##### Request +``` +{ + "nft_info": { + "token_id": "ID_of_the_token_being_queried" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | + +##### Response +``` +{ + "nft_info": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + } +} +``` +| Name | Type | Description | Optional | +|-----------|-------------------------------------|--------------------------------------------------------------------------------------|----------| +| token_uri | string | Uri pointing to off-chain JSON metadata | yes | +| extension | [Extension (see above)](#extension) | Data structure defining on-chain metadata | yes | +At most, one of the fields `token_uri` OR `extension` will be defined. + +## AllNftInfo +AllNftInfo displays the result of both [OwnerOf](#ownerof) and [NftInfo](#nftinfo) in a single query. This is provided for CW-721 compliance, but for more complete information about a token, use [NftDossier](#nftdossier), which will include private metadata and view_owner and view_private_metadata approvals if the querier is permitted to view this information. + +##### Request +``` +{ + "all_nft_info": { + "token_id": "ID_of_the_token_being_queried", + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | +| include_expired | bool | True if expired transfer approvals should be included in the response | yes | false | + +##### Response +``` +{ + "all_nft_info": { + "access": { + "owner": "address_of_the_token_owner", + "approvals": [ + { + "spender": "address_with_transfer_approval", + "expires": "never" | {"at_height": 999999} | {"at_time":999999} + }, + { + "...": "..." + } + ] + }, + "info": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + } + } +} +``` +| Name | Type | Description | Optional | +|-------------|---------------------------------------------------|-----------------------------------------------------------------------------|----------| +| access | [Cw721OwnerOfResponse (see below)](#cw721ownerof) | The token's owner and its transfer approvals if permitted to view this info | no | +| info | [Metadata (see above)](#metadata) | The token's public metadata | yes | + +### Cw721OwnerOfResponse +The Cw721OwnerOfResponse object is used to display a token's owner if the querier has view_owner permission, and the token's transfer approvals if the querier is the token's owner. +``` +{ + "owner": "address_of_the_token_owner", + "approvals": [ + { + "spender": "address_with_transfer_approval", + "expires": "never" | {"at_height": 999999} | {"at_time":999999} + }, + { + "...": "..." + } + ] +} +``` +| Name | Type | Description | Optional | +|-----------|------------------------------------------------------|----------------------------------------------------------|----------| +| owner | string (Addr) | Address of the token's owner | yes | +| approvals | array of [Cw721Approval (see above)](#cw721approval) | List of approvals to transfer this token | no | + +## PrivateMetadata +PrivateMetadata returns the private metadata of a token if the querier is permitted to view it. It follows CW-721 metadata specification, which is based on ERC-721 Metadata JSON Schema. At most, one of the fields `token_uri` OR `extension` will be defined. If the metadata is [sealed](#enablesealed), no one is permitted to view it until it has been unwrapped with [Reveal](#reveal). If no [viewer](#viewerinfo) is provided, PrivateMetadata will only display the private metadata if the private metadata is public for this token. + +##### Request +``` +{ + "private_metadata": { + "token_id": "ID_of_the_token_being_queried", + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | + +##### Response +``` +{ + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + } +} +``` +| Name | Type | Description | Optional | +|-----------|-------------------------------------|--------------------------------------------------------------------------------------|----------| +| token_uri | string | Uri pointing to off-chain JSON metadata | yes | +| extension | [Extension (see above)](#extension) | Data structure defining on-chain metadata | yes | +At most, one of the fields `token_uri` OR `extension` will be defined. + +## NftDossier +NftDossier returns all the information about a token that the viewer is permitted to view. If no [viewer](#viewerinfo) is provided, NftDossier will only display the information that has been made public. The response may include the owner, the public metadata, the private metadata, the reason the private metadata is not viewable, the royalty information, the mint run information, whether the token is transferable, whether ownership is public, whether the private metadata is public, and (if the querier is the owner,) the approvals for this token as well as the inventory-wide approvals for the owner. This implementation will only display a token's royalty recipient addresses if the querier has permission to transfer the token. + +[SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) adds a `transferable` field to the NftDossier response. + +SNIP-723 (specification to be written) adds an `unwrapped` field which is false if private metadata for this token is sealed. + +##### Request +``` +{ + "nft_dossier": { + "token_id": "ID_of_the_token_being_queried", + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | +| include_expired | bool | True if expired approvals should be included in the response | yes | false | + +##### Response +``` +{ + "nft_dossier": { + "owner": "address_of_the_token_owner", + "public_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "private_metadata": { + "token_uri": "optional_uri_pointing_to_off-chain_JSON_metadata", + "extension": { + "...": "..." + } + }, + "display_private_metadata_error": "optional_error_describing_why_private_metadata_is_not_viewable_if_applicable", + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "optional_address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + }, + "mint_run_info": { + "collection_creator": "optional_address_that_instantiated_this_contract", + "token_creator": "optional_address_that_minted_this_token", + "time_of_minting": 999999, + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, + }, + "transferable": true | false, + "unwrapped": true | false, + "owner_is_public": true | false, + "public_ownership_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "private_metadata_is_public": true | false, + "private_metadata_is_public_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "token_approvals": [ + { + "address": "whitelisted_address", + "view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + }, + { + "...": "..." + } + ], + "inventory_approvals": [ + { + "address": "whitelisted_address", + "view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + }, + { + "...": "..." + } + ] + } +} +``` +| Name | Type | Description | Optional | +|---------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------|----------| +| owner | string (Addr) | Address of the token's owner | yes | +| public_metadata | [Metadata (see above)](#metadata) | The token's public metadata | yes | +| private_metadata | [Metadata (see above)](#metadata) | The token's private metadata | yes | +| display_private_metadata_error | string | If the private metadata is not displayed, the corresponding error message | yes | +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | The token's RoyaltyInfo | yes | +| mint_run_info | [MintRunInfo (see below)](#mintruninfo) | The token's MintRunInfo | yes | +| transferable | bool | True if this token is transferable | no* | +| unwrapped | bool | False if this token's private metadata is sealed | no* | +| owner_is_public | bool | True if ownership is public for this token | no | +| public_ownership_expiration | [Expiration (see above)](#expiration) | When public ownership expires for this token. Can be a blockheight, time, or never | yes | +| private_metadata_is_public | bool | True if private metadata is public for this token | no | +| private_metadata_is_public_expiration | [Expiration (see above)](#expiration) | When public display of private metadata expires. Can be a blockheight, time, or never | yes | +| token_approvals | array of [Snip721Approval (see below)](#snipapproval) | List of approvals for this token | yes | +| inventory_approvals | array of [Snip721Approval (see below)](#snipapproval) | List of inventory-wide approvals for the token's owner | yes | + +The `transferable` field is mandatory for [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) compliant contracts, but because SNIP-722 is an optional extension to SNIP-721, any NftDossier response that does not include the field can be considered to come from a contract that only implements transferable tokens (considered equivalent to `transferable` = true). +The `unwrapped` field is mandatory for SNIP-723 (specification to be written) compliant contracts, but because SNIP-723 is an optional extension to SNIP-721, an NftDossier response might not include the field. In this case, the `display_private_metadata_error` field might indicate that the private metadata is sealed if the querier has permission to normally view private metadata. Or an [IsUnwrapped](#IsUnwrapped) query may be performed to learn the token's sealed status. + +### MintRunInfo +MintRunInfo contains information about the minting of this token. +``` +{ + "collection_creator": "optional_address_that_instantiated_this_contract", + "token_creator": "optional_address_that_minted_this_token", + "time_of_minting": 999999, + "mint_run": 3, + "serial_number": 67, + "quantity_minted_this_run": 1000, +} +``` +| Name | Type | Description | Optional | +|--------------------------|--------------------|-----------------------------------------------------------------------------------------------------------|----------| +| collection_creator | string (Addr) | The address that instantiated this contract | yes | +| token_creator | string (Addr) | The address that minted this token | yes | +| time_of_minting | number (u64) | The number of seconds since 01/01/1970 that this token was minted | yes | +| mint_run | number (u32) | The mint run this token was minted in. This represents batches of NFTs released at the same time | yes | +| serial_number | number (u32) | The serial number of this token | yes | +| quantity_minted_this_run | number (u32) | The number of tokens minted in this mint run. | yes | + +A mint run is a group of NFTs released at the same time. So, for example, if a creator decided to make 100 copies, they would all be part of mint run number 1. If they sell well and the creator wants to rerelease that NFT, he could make 100 more copies that would all be part of mint run number 2. The combination of mint_run, serial_number, and quantity_minted_this_run is used to indicate, for example, that this token was number 67 of 1000 minted in mint run number 3. + +### Snip721Approval +The Snip721Approval object is used to display all the approvals (and their expirations) that have been granted to a whitelisted address. The expiration field will be null if the whitelisted address does not have that corresponding permission type. +``` +{ + "address": "whitelisted_address", + "view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, +} +``` +| Name | Type | Description | Optional | +|----------------------------------|---------------------------------------|---------------------------------------------------------------------------------------------|----------| +| address | string (Addr) | The whitelisted address | no | +| view_owner_expiration | [Expiration (see above)](#expiration) | The expiration for view_owner permission. Can be a blockheight, time, or never | yes | +| view_private_metadata_expiration | [Expiration (see above)](#expiration) | The expiration for view__private_metadata permission. Can be a blockheight, time, or never | yes | +| transfer_expiration | [Expiration (see above)](#expiration) | The expiration for transfer permission. Can be a blockheight, time, or never | yes | + +## RoyaltyInfo (query) +If a `token_id` is provided in the request, RoyaltyInfo returns the royalty information for that token. This implementation will only display a token's royalty recipient addresses if the querier has permission to transfer the token. If no `token_id` is requested, RoyaltyInfo displays the default royalty information for the contract. This implementation will only display the contract's default royalty recipient addresses if the querier is an authorized minter. + +##### Request +``` +{ + "royalty_info": { + "token_id": "optional_ID_of_the_token_being_queried", + "viewer": { + "address": "address_of_the_querier_if_supplying_optional_ViewerInfo", + "viewing_key": "viewer's_key_if_supplying_optional_ViewerInfo" + }, + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|---------------------------------------|-----------------------------------------------------------------------|----------|--------------------------------------| +| token_id | string | ID of the token being queried | yes | query contract's default RoyaltyInfo | +| viewer | [ViewerInfo (see above)](#viewerinfo) | The address and viewing key performing this query | yes | nothing | + +##### Response +``` +{ + "royalty_info": { + "decimal_places_in_rates": 4, + "royalties": [ + { + "recipient": "optional_address_that_should_be_paid_this_royalty", + "rate": 100, + }, + { + "...": "..." + } + ], + } +} +``` +| Name | Type | Description | Optional | +|--------------|-----------------------------------------|----------------------------------------------------------------------------------------|----------| +| royalty_info | [RoyaltyInfo (see above)](#royaltyinfo) | The token or default RoyaltyInfo as per the request | yes | + +## TokenApprovals +TokenApprovals returns whether the owner and private metadata of a token is public, and lists all the approvals specific to this token. Only the token's owner may perform TokenApprovals. + +##### Request +``` +{ + "token_approvals": { + "token_id": "ID_of_the_token_being_queried", + "viewing_key": "the_token_owner's_viewing_key", + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|--------|-----------------------------------------------------------------------|----------|------------------| +| token_id | string | ID of the token being queried | no | | +| viewing_key | string | The token owner's viewing key | no | | +| include_expired | bool | True if expired approvals should be included in the response | yes | false | + +##### Response +``` +{ + "token_approvals": { + "owner_is_public": true | false, + "public_ownership_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "private_metadata_is_public": true | false, + "private_metadata_is_public_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "token_approvals": [ + { + "address": "whitelisted_address", + "view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + }, + { + "...": "..." + } + ], + } +} +``` +| Name | Type | Description | Optional | +|---------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------|----------| +| owner_is_public | bool | True if ownership is public for this token | no | +| public_ownership_expiration | [Expiration (see above)](#expiration) | When public ownership expires for this token. Can be a blockheight, time, or never | yes | +| private_metadata_is_public | bool | True if private metadata is public for this token | no | +| private_metadata_is_public_expiration | [Expiration (see above)](#expiration) | When public display of private metadata expires. Can be a blockheight, time, or never | yes | +| token_approvals | array of [Snip721Approval (see above)](#snipapproval) | List of approvals for this token | no | + +## ApprovedForAll +ApprovedForAll displays all the addresses that have approval to transfer all of the specified owner's tokens. This is provided to comply with CW-721 specification, but because approvals are private on Secret Network, if the `owner`'s viewing key is not provided, no approvals will be displayed. For a more complete list of inventory-wide approvals, the owner should use [InventoryApprovals](#inventoryapprovals) which also includes view_owner and view_private_metadata approvals. + +##### Request +``` +{ + "approved_for_all": { + "owner": "address_whose_approvals_are_being_queried", + "viewing_key": "owner's_viewing_key" + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|--------------------|------------------------------------------------------------------------------------------|----------|------------------| +| owner | string (Addr) | The address whose approvals are being queried | no | | +| viewing_key | string | The owner's viewing key | yes | nothing | +| include_expired | bool | True if expired approvals should be included in the response | yes | false | + +##### Response +``` +{ + "approved_for_all": { + "operators": [ + { + "spender": "address_with_transfer_approval", + "expires": "never" | {"at_height": 999999} | {"at_time":999999} + }, + { + "...": "..." + } + ] + } +} +``` +| Name | Type | Description | Optional | +|-----------|------------------------------------------------------|----------------------------------------------------------|----------| +| operators | array of [Cw721Approval (see above)](#cw721approval) | List of approvals to transfer all of the owner's tokens | no | + +## InventoryApprovals +InventoryApprovals returns whether all the address' tokens have public ownership and/or public display of private metadata, and lists all the inventory-wide approvals the address has granted. Only the viewing key for this specified address will be accepted. + +##### Request +``` +{ + "inventory_approvals": { + "address": "address_whose_approvals_are_being_queried", + "viewing_key": "the_viewing_key_associated_with_this_address", + "include_expired": true | false + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------------|--------------------|-----------------------------------------------------------------------|----------|------------------| +| address | string (Addr) | The address whose inventory-wide approvals are being queried | no | | +| viewing_key | string | The viewing key associated with this address | no | | +| include_expired | bool | True if expired approvals should be included in the response | yes | false | + +##### Response +``` +{ + "inventory_approvals": { + "owner_is_public": true | false, + "public_ownership_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "private_metadata_is_public": true | false, + "private_metadata_is_public_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "inventory_approvals": [ + { + "address": "whitelisted_address", + "view_owner_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "view_private_metadata_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + "transfer_expiration": "never" | {"at_height": 999999} | {"at_time":999999}, + }, + { + "...": "..." + } + ], + } +} +``` +| Name | Type | Description | Optional | +|---------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------------------------|----------| +| owner_is_public | bool | True if ownership is public for all of this address' tokens | no | +| public_ownership_expiration | [Expiration (see above)](#expiration) | When public ownership expires for all tokens. Can be a blockheight, time, or never | yes | +| private_metadata_is_public | bool | True if private metadata is public for all of this address' tokens | no | +| private_metadata_is_public_expiration | [Expiration (see above)](#expiration) | When public display of private metadata expires. Can be a blockheight, time, or never | yes | +| inventory_approvals | array of [Snip721Approval (see above)](#snipapproval) | List of inventory-wide approvals for this address | no | + +## Tokens +Tokens displays an optionally paginated list of all the token IDs that belong to the specified `owner`. It will only display the owner's tokens on which the querier has view_owner permission. If no viewing key is provided, it will only display the owner's tokens that have public ownership. When paginating, supply the last token ID received in a response as the `start_after` string of the next query to continue listing where the previous query stopped. + +##### Request +``` +{ + "tokens": { + "owner": "address_whose_inventory_is_being_queried", + "viewer": "address_of_the_querier_if_different_from_owner", + "viewing_key": "querier's_viewing_key" + "start_after": "optionally_display_only_token_ids_that_come_after_this_one_in_the_list", + "limit": 10 + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------------------|------------------------------------------------------------------------------------------|----------|------------------| +| owner | string (Addr) | The address whose inventory is being queried | no | | +| viewer | string (Addr) | The querier's address if different from the `owner` | yes | nothing | +| viewing_key | string | The querier's viewing key | yes | nothing | +| start_after | string | Results will only list token IDs that come after this token ID in the list | yes | nothing | +| limit | number (u32) | Number of token IDs to return | yes | 30 | + +##### Response +``` +{ + "token_list": { + "tokens": [ + "list", "of", "the", "owner's", "tokens", "..." + ] + } +} +``` +| Name | Type | Description | Optional | +|---------|-----------------|----------------------------------------------------------------------|----------| +| tokens | array of string | A list of token IDs owned by the specified `owner` | no | + +## VerifyTransferApproval +VerifyTransferApproval will verify that the specified address has approval to transfer the entire provided list of tokens. As explained [above](#queryblockinfo), queries may experience a delay in revealing expired approvals, so it is possible that a transfer attempt will still fail even after being verified by VerifyTransferApproval. If the address does not have transfer approval on all the tokens, the response will indicate the first token encountered that can not be transferred by the address. + +Because the intent of VerifyTransferApproval is to provide contracts a way to know before-hand whether an attempt to transfer tokens will fail, this implementation will consider any [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) non-transferable token as unapproved for transfer. + +##### Request +``` +{ + "verify_transfer_approval": { + "token_ids": [ + "list", "of", "tokens", "to", "check", "for", "transfer", "approval", "..." + ], + "address": "address_to_use_for_approval_checking", + "viewing_key": "address'_viewing_key" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------------------|------------------------------------------------------------------------------------------|----------|------------------| +| token_ids | array of string | List of tokens to check for the address' transfer approval | no | | +| address | string (Addr) | Address being checked for transfer approval | no | | +| viewing_key | string | The address' viewing key | no | | + +##### Response +``` +{ + "verify_transfer_approval": { + "approved_for_all": true | false, + "first_unapproved_token": "token_id" + } +} +``` +| Name | Type | Description | Optional | +|------------------------|--------|-----------------------------------------------------------------------------------|----------| +| approved_for_all | bool | True if the `address` has transfer approval on all the `token_ids` | no | +| first_unapproved_token | string | The first token in the list that the `address` does not have approval to transfer | yes | + +## ImplementsTokenSubtype +ImplementsTokenSubtype is a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) query which indicates whether the contract implements the `token_subtype` Extension field. Because legacy SNIP-721 contracts do not implement this query and do not implement token subtypes, any use of this query should always check for an error response, and if the response is an error, it can be considered that the contract does not implement subtypes. Because message parsing ignores input fields that a contract does not expect, this query should be used before attempting a message that uses the `token_subtype` [Extension](#extension) field. If the message is sent to a SNIP-721 contract that does not implement `token_subtype`, that field will just be ignored and the resulting NFT will still be created/updated, but without a `token_subtype`. + +##### Request +``` +{ + "implements_token_subtype": {} +} +``` +##### Response +``` +{ + "implements_token_subtype": { + "is_enabled": true | false + } +} +``` +| Name | Type | Description | Optional | +|-------------|------|-------------------------------------------------------------------------|----------| +| is_enabled | bool | True if the contract implements token subtypes | no | + +## ImplementsNonTransferableTokens +ImplementsNonTransferableTokens is a [SNIP-722](https://github.com/baedrik/snip-722-spec/blob/master/SNIP-722.md) query which indicates whether the contract implements non-transferable tokens. Because legacy SNIP-721 contracts do not implement this query and do not implement non-transferable tokens, any use of this query should always check for an error response, and if the response is an error, it can be considered that the contract does not implement non-transferable tokens. Because message parsing ignores input fields that a contract does not expect, this query should be used before attempting to mint a non-transferable token. If the message is sent to a SNIP-721 contract that does not implement non-transferable tokens, the `transferable` field will just be ignored and the resulting NFT will still be created, but will always be transferable. + +##### Request +``` +{ + "implements_non_transferable_tokens": {} +} +``` +##### Response +``` +{ + "implements_non_transferable_tokens": { + "is_enabled": true | false + } +} +``` +| Name | Type | Description | Optional | +|-------------|------|-------------------------------------------------------------------------|----------| +| is_enabled | bool | True if the contract implements non-transferable tokens | no | + +## TransactionHistory +TransactionHistory displays an optionally paginated list of transactions (mint, burn, and transfer) in reverse chronological order that involve the specified address. + +##### Request +``` +{ + "transaction_history": { + "address": "address_whose_tx_history_is_being_queried", + "viewing_key": "address'_viewing_key" + "page": "optional_page_to_display", + "page_size": 10 + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-------------|--------------------|-----------------------------------------------------------------------------------------------------------------------|----------|------------------| +| address | string (Addr) | The address whose transaction history is being queried | no | | +| viewing_key | string | The address' viewing key | no | | +| page | number (u32) | The page number to display, where the first transaction shown skips the `page` * `page_size` most recent transactions | yes | 0 | +| page_size | number (u32) | Number of transactions to return | yes | 30 | + +##### Response +``` +{ + "transaction_history": { + "total": 99, + "txs": [ + { + "tx_id": 9999, + "block_height": 999999, + "block_time": 1610000012, + "token_id": "token_involved_in_the_tx", + "action": { + "transfer": { + "from": "previous_owner_of_the_token", + "sender": "address_that_sent_the_token_if_different_than_the_previous_owner", + "recipient": "new_owner_of_the_token" + } + }, + "memo": "optional_memo_for_the_tx" + }, + { + "tx_id": 9998, + "block_height": 999998, + "block_time": 1610000006, + "token_id": "token_involved_in_the_tx", + "action": { + "mint": { + "minter": "address_that_minted_the_token", + "recipient": "owner_of_the_newly_minted_token" + } + }, + "memo": "optional_memo_for_the_tx" + }, + { + "tx_id": 9997, + "block_height": 999997, + "block_time": 1610000000, + "token_id": "token_involved_in_the_tx", + "action": { + "burn": { + "owner": "previous_owner_of_the_token", + "burner": "address_that_burned_the_token_if_different_than_the_previous_owner", + } + }, + "memo": "optional_memo_for_the_tx" + }, + { + "...": "..." + } + ], + } +} +``` +| Name | Type | Description | Optional | +|-------|--------------------------------|----------------------------------------------------------------------------------------|----------| +| total | number (u64) | The total number of transactions that involve the specified address | no | +| txs | array of [Tx (see below)](#tx) | List of transactions in reverse chronological order that involve the specified address | no | + +### Tx +The Tx object contains all the information pertaining to a [mint](#txmint), [burn](#txburn), or [transfer](#txxfer) transaction. +``` +{ + "tx_id": 9999, + "block_height": 999999, + "block_time": 1610000000, + "token_id": "token_involved_in_the_tx", + "action": { TxAction::Transfer | TxAction::Mint | TxAction::Burn }, + "memo": "optional_memo_for_the_tx" +} +``` +| Name | Type | Description | Optional | +|--------------|-----------------------------------|-------------------------------------------------------------------------------------------|----------| +| tx_id | number (u64) | The transaction identifier | no | +| block_height | number (u64) | The number of the block that contains the transaction | no | +| block_time | number (u64) | The time in seconds since 01/01/1970 of the block that contains the transaction | no | +| token_id | string | The token involved in the transaction | no | +| action | [TxAction (see below)](#txaction) | The type of transaction and the information specific to that type | no | +| memo | string | `memo` for the transaction that is only viewable by addresses involved in the transaction | yes | + +### TxAction +The TxAction object defines the type of transaction and holds the information specific to that type. + +* TxAction::Mint +``` +{ + "minter": "address_that_minted_the_token", + "recipient": "owner_of_the_newly_minted_token" +} + +``` +| Name | Type | Description | Optional | +|-----------|--------------------|--------------------------------------------------------------------------------|----------| +| minter | string (Addr) | The address that minted the token | no | +| recipient | string (Addr) | The address of the newly minted token's owner | no | + +* TxAction::Transfer +``` +{ + "from": "previous_owner_of_the_token", + "sender": "address_that_sent_the_token_if_different_than_the_previous_owner", + "recipient": "new_owner_of_the_token" +} + +``` +| Name | Type | Description | Optional | +|-----------|--------------------|--------------------------------------------------------------------------------|----------| +| from | string (Addr) | The previous owner of the token | no | +| sender | string (Addr) | The address that sent the token if different than the previous owner | yes | +| recipient | string (Addr) | The new owner of the token | no | + +* TxAction::Burn +``` +{ + "owner": "previous_owner_of_the_token", + "burner": "address_that_burned_the_token_if_different_than_the_previous_owner", +} + +``` +| Name | Type | Description | Optional | +|-----------|--------------------|--------------------------------------------------------------------------------|----------| +| owner | string (Addr) | The previous owner of the token | no | +| burner | string (Addr) | The address that burned the token if different than the previous owner | yes | + +## WithPermit +SNIP-721 contracts may optionally implement query permits as specified in [SNIP-24](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md). They are an improvement over viewing keys in that permits allow a user to query private information without first needing to send a transaction to set or create a viewing key (see [here](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md#Rationale) for more details). + +Because SNIP-721s already provide whitelisting functionality for approving other addresses to view private information, SNIP-721 permits typically use the `owner` permission type to authenticate the query to display all the private information that the address of the creator of the permit is authorized to see. So, it is generally advised that you never give SNIP-721 permits with `owner` permission to anyone. If you need someone to view private information of a token you own, you should whitelist their address, and they will then use a permit they create themselves to view only what you have approved. This eliminates the need to provide them a permit, eliminates the need to track permit names in order to later revoke viewing permission, and provides an easy way to query the network to see everyone that currently has viewing approval. That said, contract developers are not limited, and may choose, if appropriate for their use-case, to implement permits that have more granular permissions that users are meant to share with others. + +WithPermit wraps permit queries in the [same manner](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md#WithPermit) as SNIP-24. + +##### Request +``` +{ + "with_permit": { + "permit": { + "params": { + "permit_name": "some_name", + "allowed_tokens": ["collection_address_1", "collection_address_2", "..."], + "chain_id": "some_chain_id", + "permissions": ["owner"] + }, + "signature": { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "33_bytes_of_secp256k1_pubkey_as_base64" + }, + "signature": "64_bytes_of_secp256k1_signature_as_base64" + } + }, + "query": { + "QueryWithPermit_variant_defined_below": { "...": "..." } + } + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|--------|---------------------------------------------------------------------------------------|-----------------------------------------------|----------|------------------| +| permit | [Permit](https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-24.md#WithPermit) | A permit following SNIP-24 standard | no | | +| query | [QueryWithPermit (see below)](#QueryWithPermit) | The query to perform and its input parameters | no | | + +#### QueryWithPermit +QueryWithPermit is an enum whose variants correlate with all SNIP-721 queries that require authentication. The input parameters are the same as the corresponding query other than the absence of [ViewerInfo](#viewerinfo) and viewing keys because the permit supplied with the `WithPermit` query provides both the address and authentication. + +* NumTokens ([corresponding query](#NumTokens)) +##### WithPermit `query` Parameter +``` +"query": { + "num_tokens": {} +} +``` +* AllTokens ([corresponding query](#AllTokens)) +##### WithPermit `query` Parameter +``` +"query": { + "all_tokens": { + "start_after": "optionally_display_only_token_ids_that_come_after_this_one_in_the_list", + "limit": 10 + } +} +``` +* OwnerOf ([corresponding query](#OwnerOf)) +##### WithPermit `query` Parameter +``` +"query": { + "owner_of": { + "token_id": "ID_of_the_token_being_queried", + "include_expired": true | false + } +} +``` +* AllNftInfo ([corresponding query](#allnftinfo)) +##### WithPermit `query` Parameter +``` +"query": { + "all_nft_info": { + "token_id": "ID_of_the_token_being_queried", + "include_expired": true | false + } +} +``` +* PrivateMetadata ([corresponding query](#PrivateMetadata)) +##### WithPermit `query` Parameter +``` +"query": { + "private_metadata": { + "token_id": "ID_of_the_token_being_queried", + } +} +``` +* NftDossier ([corresponding query](#NftDossier)) +##### WithPermit `query` Parameter +``` +"query": { + "nft_dossier": { + "token_id": "ID_of_the_token_being_queried", + "include_expired": true | false + } +} +``` +* BatchNftDossier ([corresponding query](#BatchNftDossier)) +##### WithPermit `query` Parameter +``` +"query": { + "batch_nft_dossier": { + "token_ids": ["ID_of", "the_tokens", "being_queried"], + "include_expired": true | false + } +} +``` +* RoyaltyInfo ([corresponding query](#royaltyquery)) +##### WithPermit `query` Parameter +``` +"query": { + "royalty_info": { + "token_id": "optional_ID_of_the_token_being_queried", + } +} +``` +* TokenApprovals ([corresponding query](#TokenApprovals)) +##### WithPermit `query` Parameter +``` +"query": { + "token_approvals": { + "token_id": "ID_of_the_token_being_queried", + "include_expired": true | false + } +} +``` +* ApprovedForAll ([corresponding query](#ApprovedForAll)) +##### WithPermit `query` Parameter +``` +"query": { + "approved_for_all": { + "include_expired": true | false + } +} +``` +* InventoryApprovals ([corresponding query](#InventoryApprovals)) +##### WithPermit `query` Parameter +``` +"query": { + "inventory_approvals": { + "include_expired": true | false + } +} +``` +* NumTokensOfOwner ([corresponding query](#NumTokensOfOwner)) +##### WithPermit `query` Parameter +``` +"query": { + "num_tokens_of_owner": { + "owner": "address_whose_token_count_is_being_queried", + } +} +``` +* Tokens ([corresponding query](#tokens)) +##### WithPermit `query` Parameter +``` +"query": { + "tokens": { + "owner": "address_whose_inventory_is_being_queried", + "start_after": "optionally_display_only_token_ids_that_come_after_this_one_in_the_list", + "limit": 10 + } +} +``` +* VerifyTransferApproval ([corresponding query](#VerifyTransferApproval)) +##### WithPermit `query` Parameter +``` +"query": { + "verify_transfer_approval": { + "token_ids": [ + "list", "of", "tokens", "to", "check", "for", "transfer", "approval", "..." + ], + } +} +``` +* TransactionHistory ([corresponding query](#TransactionHistory)) +##### WithPermit `query` Parameter +``` +"query": { + "transaction_history": { + "page": "optional_page_to_display", + "page_size": 10 + } +} +``` + +# Receiver Interface +When the token contract executes [SendNft](#sendnft) and [BatchSendNft](#batchsend) messages, it will perform a callback to the receiving contract's receiver interface if the contract had registered its code hash using [RegisterReceiveNft](#registerreceive). [BatchReceiveNft](#batchreceivenft) is preferred over [ReceiveNft](#receivenft), because ReceiveNft does not allow the recipient to know who sent the token, only its previous owner, and ReceiveNft can only process one token. So it is inefficient when sending multiple tokens to the same contract (a deck of game cards for instance). ReceiveNft primarily exists just to maintain CW-721 compliance, and if the receiving contract registered that it implements BatchReceiveNft, BatchReceiveNft will be called, even when there is only one token_id in the message. + +Also, it should be noted that the CW-721 `sender` field is inaccurately named, because it is used to hold the address the token came from, not the address that sent it (which is not always the same). The name is reluctantly kept in [ReceiveNft](#receivenft) to maintain CW-721 compliance, but BatchReceiveNft uses `sender` to hold the sending address (which matches both its true role and its SNIP-20 Receive counterpart). Any contract that is implementing both Receiver Interfaces must be sure that the ReceiveNft `sender` field is actually processed like a BatchReceiveNft `from` field. Again, apologies for any confusion caused by propagating inaccuracies, but because [InterNFT](https://internft.org) is planning on using CW-721 standards, compliance with CW-721 might be necessary. + +## ReceiveNft +ReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver interface. [BatchReceiveNft](#batchreceivenft), which is more informative and more efficient, is preferred over ReceiveNft. +``` +{ + "receive_nft": { + "sender": "address_of_the_previous_owner_of_the_token", + "token_id": "ID_of_the_sent_token", + "msg": "optional_base64_encoded_Binary_message_used_to_control_receiving_logic" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|----------|--------------------------------|--------------------------------------------------------------------------------------------------------|----------|------------------| +| sender | string (Addr) | Address of the token's previous owner ([see above](#cwsender) about this inaccurate naming convention) | no | | +| token_id | string | ID of the sent token | no | | +| msg | string (base64 encoded Binary) | Msg used to control receiving logic | yes | nothing | + +## BatchReceiveNft +BatchReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver interface. BatchReceiveNft, which is more informative and more efficient, is preferred over [ReceiveNft](#receivenft). +``` +{ + "batch_receive_nft": { + "sender": "address_that_sent_the_tokens", + "from": "address_of_the_previous_owner_of_the_tokens", + "token_ids": [ + "list", "of", "tokens", "sent", "..." + ], + "msg": "optional_base64_encoded_Binary_message_used_to_control_receiving_logic" + } +} +``` +| Name | Type | Description | Optional | Value If Omitted | +|-----------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------|----------|------------------| +| sender | string (Addr) | Address that sent the tokens (this field has no ReceiveNft equivalent, [see above](#cwsender)) | no | | +| from | string (Addr) | Address of the tokens' previous owner (this field is equivalent to the ReceiveNft `sender` field, [see above](#cwsender))| no | | +| token_ids | array of string | List of the tokens sent | no | | +| msg | string (base64 encoded Binary) | Msg used to control receiving logic | yes | nothing | diff --git a/contracts/external/snip721-reference-impl/rustfmt.toml b/contracts/external/snip721-reference-impl/rustfmt.toml new file mode 100644 index 0000000..11a85e6 --- /dev/null +++ b/contracts/external/snip721-reference-impl/rustfmt.toml @@ -0,0 +1,15 @@ +# stable +newline_style = "unix" +hard_tabs = false +tab_spaces = 4 + +# unstable... should we require `rustup run nightly cargo fmt` ? +# or just update the style guide when they are stable? +#fn_single_line = true +#format_code_in_doc_comments = true +#overflow_delimited_expr = true +#reorder_impl_items = true +#struct_field_align_threshold = 20 +#struct_lit_single_line = true +#report_todo = "Always" + diff --git a/contracts/external/snip721-reference-impl/src/contract.rs b/contracts/external/snip721-reference-impl/src/contract.rs index dc74ec1..fba3973 100644 --- a/contracts/external/snip721-reference-impl/src/contract.rs +++ b/contracts/external/snip721-reference-impl/src/contract.rs @@ -16,6 +16,8 @@ use secret_toolkit::{ viewing_key::{ViewingKey, ViewingKeyStore}, }; +use crate::expiration::Expiration; +use crate::inventory::{Inventory, InventoryIter}; use crate::mint_run::{SerialNumber, StoredMintRunInfo}; use crate::msg::{ AccessLevel, BatchNftDossierElement, Burn, ContractStatus, Cw721Approval, Cw721OwnerOfResponse, @@ -33,14 +35,6 @@ use crate::state::{ PREFIX_ROYALTY_INFO, VIEWING_KEY_ERR_MSG, }; use crate::token::{Metadata, Token}; -use crate::{ - expiration::Expiration, - msg::{InstantiateResponse, Minters, NftInfo, NumTokens, OwnerOf}, -}; -use crate::{ - inventory::{Inventory, InventoryIter}, - msg::ContractInfo, -}; /// pad handle responses and log attributes to blocks of 256 bytes to prevent leaking info based on /// response size @@ -62,7 +56,7 @@ pub const ID_BLOCK_SIZE: u32 = 64; #[entry_point] pub fn instantiate( deps: DepsMut, - env: Env, + _env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> StdResult { @@ -128,12 +122,7 @@ pub fn instantiate( } else { Vec::new() }; - Ok(Response::new() - .add_messages(messages) - .set_data(to_binary(&InstantiateResponse { - contract_address: env.contract.address, - code_hash: env.contract.code_hash, - })?)) + Ok(Response::new().add_messages(messages)) } ///////////////////////////////////// Handle ////////////////////////////////////// @@ -1967,7 +1956,7 @@ pub fn query_contract_creator(deps: Deps) -> StdResult { pub fn query_contract_info(storage: &dyn Storage) -> StdResult { let config: Config = load(storage, CONFIG_KEY)?; - to_binary(&ContractInfo { + to_binary(&QueryAnswer::ContractInfo { name: config.name, symbol: config.symbol, }) @@ -2073,7 +2062,7 @@ pub fn query_config(storage: &dyn Storage) -> StdResult { pub fn query_minters(deps: Deps) -> StdResult { let minters: Vec = may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); - to_binary(&Minters { + to_binary(&QueryAnswer::Minters { minters: minters .iter() .map(|m| deps.api.addr_humanize(m)) @@ -2096,7 +2085,7 @@ pub fn query_num_tokens( // authenticate permission to view token supply check_view_supply(deps, viewer, from_permit)?; let config: Config = load(deps.storage, CONFIG_KEY)?; - to_binary(&NumTokens { + to_binary(&QueryAnswer::NumTokens { count: config.token_cnt, }) } @@ -2169,7 +2158,7 @@ pub fn query_owner_of( let (may_owner, approvals, _idx) = process_cw721_owner_of(deps, block, token_id, viewer, include_expired, from_permit)?; if let Some(owner) = may_owner { - return to_binary(&OwnerOf { owner, approvals }); + return to_binary(&QueryAnswer::OwnerOf { owner, approvals }); } Err(StdError::generic_err(format!( "You are not authorized to view the owner of token {}", @@ -2193,7 +2182,7 @@ pub fn query_nft_info(storage: &dyn Storage, token_id: &str) -> StdResult = Vec::new(); for id in token_ids.into_iter() { // cargo fmt creates the and_then block, but clippy doesn't like it - #[allow(clippy::blocks_in_conditions)] + #[allow(clippy::blocks_in_if_conditions)] if get_token_if_permitted( deps, block, @@ -4108,7 +4096,6 @@ pub struct CacheReceiverInfo { #[allow(clippy::too_many_arguments)] fn receiver_callback_msgs( deps: &mut DepsMut, - _env: Env, contract_human: &str, contract: &CanonicalAddr, receiver_info: Option, @@ -4410,7 +4397,6 @@ fn send_list( // get BatchReceiveNft and ReceiveNft msgs for all the tokens sent in this Send messages.extend(receiver_callback_msgs( &mut deps, - env.clone(), &send.contract, &contract_raw, send.receiver_info, diff --git a/contracts/external/snip721-reference-impl/src/msg.rs b/contracts/external/snip721-reference-impl/src/msg.rs index 51c9ed7..984d0af 100644 --- a/contracts/external/snip721-reference-impl/src/msg.rs +++ b/contracts/external/snip721-reference-impl/src/msg.rs @@ -11,7 +11,7 @@ use crate::royalties::{DisplayRoyaltyInfo, RoyaltyInfo}; use crate::token::{Extension, Metadata}; /// Instantiation message -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct InstantiateMsg { /// name of token contract pub name: String, @@ -33,12 +33,6 @@ pub struct InstantiateMsg { pub post_init_callback: Option, } -#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] -pub struct InstantiateResponse { - pub contract_address: Addr, - pub code_hash: String, -} - /// This type represents optional configuration values. /// All values are optional and have defaults which are more private by default, /// but can be overridden if necessary @@ -105,7 +99,7 @@ pub struct PostInstantiateCallback { pub send: Vec, } -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { /// mint new token @@ -413,7 +407,7 @@ pub enum ExecuteMsg { } /// permission access level -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum AccessLevel { /// approve permission only for the specified token @@ -890,29 +884,6 @@ pub struct BatchNftDossierElement { pub inventory_approvals: Option>, } -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct Minters { - pub minters: Vec, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct NftInfo { - pub token_uri: Option, - pub extension: Option, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct OwnerOf { - pub owner: Addr, - pub approvals: Vec, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct ContractInfo { - pub name: String, - pub symbol: String, -} - #[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum QueryAnswer { @@ -1025,11 +996,6 @@ pub enum QueryAnswer { }, } -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct NumTokens { - pub count: u32, -} - #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum ResponseStatus { diff --git a/contracts/external/snip721-reference-impl/src/token.rs b/contracts/external/snip721-reference-impl/src/token.rs index 848c8ab..35226fd 100644 --- a/contracts/external/snip721-reference-impl/src/token.rs +++ b/contracts/external/snip721-reference-impl/src/token.rs @@ -63,11 +63,6 @@ pub struct Extension { /// token subtypes used by Stashh for display groupings (primarily used for badges, which are specified /// by using "badge" as the token_subtype) pub token_subtype: Option, - - /// Optional on-chain role for this member, can be used by other contracts to enforce permissions - pub role: Option, - /// The voting weight of this role - pub weight: u64, } /// attribute trait diff --git a/contracts/external/snip721-reference-impl/src/unittest_handles.rs b/contracts/external/snip721-reference-impl/src/unittest_handles.rs index 4f140f8..a30b246 100644 --- a/contracts/external/snip721-reference-impl/src/unittest_handles.rs +++ b/contracts/external/snip721-reference-impl/src/unittest_handles.rs @@ -546,7 +546,7 @@ mod tests { "Init failed: {}", init_result.err().unwrap() ); - let pub_expect = Metadata { + let pub_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -554,8 +554,8 @@ mod tests { image: Some("uri".to_string()), ..Extension::default() }), - }; - let priv_expect = Metadata { + }); + let priv_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFTpriv".to_string()), @@ -563,12 +563,12 @@ mod tests { image: Some("privuri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub_expect.clone()), - private_metadata: Some(priv_expect.clone()), + public_metadata: pub_expect.clone(), + private_metadata: priv_expect.clone(), royalty_info: None, serial_number: None, transferable: None, @@ -601,10 +601,10 @@ mod tests { // verify the token metadata let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); - assert_eq!(pub_meta, pub_expect); + assert_eq!(pub_meta, pub_expect.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); - assert_eq!(priv_meta, priv_expect); + assert_eq!(priv_meta, priv_expect.unwrap()); // verify token is in owner list assert!(Inventory::owns(&deps.storage, &alice_raw, 0).unwrap()); // verify mint tx was logged to both parties @@ -661,7 +661,7 @@ mod tests { assert!(error.contains("Token ID MyNFT is already in use")); // test minting without specifying recipient or id - let pub_expect = Metadata { + let pub_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("AdminNFT".to_string()), @@ -669,11 +669,11 @@ mod tests { image: None, ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: None, owner: None, - public_metadata: Some(pub_expect.clone()), + public_metadata: pub_expect.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -714,7 +714,7 @@ mod tests { // verify metadata let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); - assert_eq!(pub_meta, pub_expect); + assert_eq!(pub_meta, pub_expect.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &token_key).unwrap(); assert!(priv_meta.is_none()); @@ -1017,7 +1017,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let set_expect = Metadata { + let set_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("New Name".to_string()), @@ -1025,10 +1025,10 @@ mod tests { image: Some("new uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::SetMetadata { token_id: "MyNFT".to_string(), - public_metadata: Some(set_expect.clone()), + public_metadata: set_expect.clone(), private_metadata: None, padding: None, }; @@ -1040,7 +1040,7 @@ mod tests { ); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &0u32.to_le_bytes()).unwrap(); - assert_eq!(pub_meta, set_expect); + assert_eq!(pub_meta, set_expect.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &0u32.to_le_bytes()).unwrap(); assert!(priv_meta.is_none()); @@ -1240,7 +1240,7 @@ mod tests { assert!(error.contains("Metadata can not have BOTH token_uri AND extension")); // sanity check, minter changing metadata after owner unwrapped - let set_pub = Metadata { + let set_pub = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("New Name Pub".to_string()), @@ -1248,8 +1248,8 @@ mod tests { image: Some("new uri pub".to_string()), ..Extension::default() }), - }; - let set_priv = Metadata { + }); + let set_priv = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("New Name Priv".to_string()), @@ -1257,11 +1257,11 @@ mod tests { image: Some("new uri priv".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::SetMetadata { token_id: "MyNFT".to_string(), - public_metadata: Some(set_pub.clone()), - private_metadata: Some(set_priv.clone()), + public_metadata: set_pub.clone(), + private_metadata: set_priv.clone(), padding: None, }; let _handle_result = execute( @@ -1272,10 +1272,10 @@ mod tests { ); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); - assert_eq!(pub_meta, set_pub); + assert_eq!(pub_meta, set_pub.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); - assert_eq!(priv_meta, set_priv); + assert_eq!(priv_meta, set_priv.unwrap()); // test setting metadata when status prevents it let execute_msg = ExecuteMsg::SetContractStatus { @@ -1355,7 +1355,7 @@ mod tests { "Init failed: {}", init_result.err().unwrap() ); - let pub_expect = Metadata { + let pub_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -1363,11 +1363,11 @@ mod tests { image: Some("uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub_expect.clone()), + public_metadata: pub_expect.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -1381,7 +1381,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let priv_expect = Metadata { + let priv_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("New Name".to_string()), @@ -1389,11 +1389,11 @@ mod tests { image: Some("new uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::SetMetadata { token_id: "MyNFT".to_string(), public_metadata: None, - private_metadata: Some(priv_expect.clone()), + private_metadata: priv_expect.clone(), padding: None, }; let _handle_result = execute( @@ -1404,10 +1404,10 @@ mod tests { ); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); - assert_eq!(priv_meta, priv_expect); + assert_eq!(priv_meta, priv_expect.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); - assert_eq!(pub_meta, pub_expect); + assert_eq!(pub_meta, pub_expect.unwrap()); } // test Reveal @@ -1549,7 +1549,7 @@ mod tests { init_result.err().unwrap() ); - let seal_meta = Metadata { + let seal_meta = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MySealedNFT".to_string()), @@ -1557,11 +1557,11 @@ mod tests { image: Some("sealed_uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(seal_meta.clone()), + private_metadata: seal_meta.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -1610,7 +1610,7 @@ mod tests { assert!(priv_meta.is_none()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); - assert_eq!(pub_meta, seal_meta.clone()); + assert_eq!(pub_meta, seal_meta.clone().unwrap()); // test trying to unwrap token that has already been unwrapped let execute_msg = ExecuteMsg::Reveal { @@ -1637,7 +1637,7 @@ mod tests { let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(seal_meta.clone()), + private_metadata: seal_meta.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -1663,7 +1663,7 @@ mod tests { ); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); - assert_eq!(priv_meta, seal_meta); + assert_eq!(priv_meta, seal_meta.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &token_key).unwrap(); assert!(pub_meta.is_none()); @@ -1726,7 +1726,7 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("You do not own token NFT1")); - let pub1 = Metadata { + let pub1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("My1".to_string()), @@ -1734,11 +1734,11 @@ mod tests { image: Some("URI 1".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("NFT1".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub1.clone()), + public_metadata: pub1.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -1752,7 +1752,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let pub2: Metadata = Metadata { + let pub2 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("My2".to_string()), @@ -1760,11 +1760,11 @@ mod tests { image: Some("URI 2".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("NFT2".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub2.clone()), + public_metadata: pub2.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -1778,7 +1778,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); // test burn when status prevents it - let pub3 = Metadata { + let pub3 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("My3".to_string()), @@ -1786,11 +1786,11 @@ mod tests { image: Some("URI 3".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("NFT3".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub3.clone()), + public_metadata: pub3.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -1804,7 +1804,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let pub4 = Metadata { + let pub4 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("My4".to_string()), @@ -1812,11 +1812,11 @@ mod tests { image: Some("URI 4".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("NFT4".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub4.clone()), + public_metadata: pub4.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -1980,7 +1980,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); @@ -2041,7 +2041,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); @@ -2115,7 +2115,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); - assert_eq!(pub_meta, pub2.clone()); + assert_eq!(pub_meta, pub2.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); assert!(priv_meta.is_none()); @@ -2176,7 +2176,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); - assert_eq!(pub_meta, pub2.clone()); + assert_eq!(pub_meta, pub2.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); assert!(priv_meta.is_none()); @@ -2371,7 +2371,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); - assert_eq!(pub_meta, pub2); + assert_eq!(pub_meta, pub2.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); assert!(priv_meta.is_none()); @@ -2477,7 +2477,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); @@ -2514,7 +2514,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -2622,7 +2622,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); @@ -2649,7 +2649,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -2732,7 +2732,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft4_key).unwrap(); - assert_eq!(pub_meta, pub4.clone()); + assert_eq!(pub_meta, pub4.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft4_key).unwrap(); assert!(priv_meta.is_none()); @@ -2846,7 +2846,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -2954,7 +2954,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -3071,7 +3071,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft4_key).unwrap(); - assert_eq!(pub_meta, pub4); + assert_eq!(pub_meta, pub4.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -3207,7 +3207,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1); + assert_eq!(pub_meta, pub1.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); @@ -3256,7 +3256,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -3729,7 +3729,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3.clone()); + assert_eq!(pub_meta, pub3.clone().unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -3778,7 +3778,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); - assert_eq!(pub_meta, pub3); + assert_eq!(pub_meta, pub3.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); assert!(priv_meta.is_none()); @@ -3845,7 +3845,7 @@ mod tests { error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") ); - let priv_expect = Metadata { + let priv_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -3853,11 +3853,11 @@ mod tests { image: Some("uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv_expect.clone()), + private_metadata: priv_expect.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -4081,7 +4081,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect.clone()); + assert_eq!(priv_meta, priv_expect.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4143,7 +4143,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect); + assert_eq!(priv_meta, priv_expect.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4171,7 +4171,7 @@ mod tests { // used to test auto-setting individual token permissions when only one token // of many is approved with a different expiration than an operator's expiration - let priv2 = Metadata { + let priv2 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT2".to_string()), @@ -4179,11 +4179,11 @@ mod tests { image: Some("uri2".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT2".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv2.clone()), + private_metadata: priv2.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -4310,7 +4310,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok2_key).unwrap(); - assert_eq!(priv_meta, priv2); + assert_eq!(priv_meta, priv2.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok2_key).unwrap(); assert!(pub_meta.is_none()); @@ -4392,7 +4392,7 @@ mod tests { error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") ); - let priv_expect = Metadata { + let priv_expect = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -4400,11 +4400,11 @@ mod tests { image: Some("uri".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv_expect.clone()), + private_metadata: priv_expect.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -4604,7 +4604,7 @@ mod tests { assert!(token.permissions.is_empty()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect.clone()); + assert_eq!(priv_meta, priv_expect.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4693,7 +4693,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect.clone()); + assert_eq!(priv_meta, priv_expect.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4727,7 +4727,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect.clone()); + assert_eq!(priv_meta, priv_expect.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4772,7 +4772,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv_expect); + assert_eq!(priv_meta, priv_expect.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); assert!(pub_meta.is_none()); @@ -4794,7 +4794,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let priv2 = Metadata { + let priv2 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT2".to_string()), @@ -4802,11 +4802,11 @@ mod tests { image: Some("uri2".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT2".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv2.clone()), + private_metadata: priv2.clone(), public_metadata: None, royalty_info: None, serial_number: None, @@ -4921,7 +4921,7 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok2_key).unwrap(); - assert_eq!(priv_meta, priv2); + assert_eq!(priv_meta, priv2.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Option = may_load(&pub_store, &tok2_key).unwrap(); assert!(pub_meta.is_none()); @@ -5327,7 +5327,7 @@ mod tests { mock_info("admin", &[]), execute_msg, ); - let priv3 = Metadata { + let priv3 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT3".to_string()), @@ -5335,8 +5335,8 @@ mod tests { image: Some("privuri3".to_string()), ..Extension::default() }), - }; - let pub3 = Metadata { + }); + let pub3 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT3".to_string()), @@ -5344,12 +5344,12 @@ mod tests { image: Some("puburi3".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT3".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv3.clone()), - public_metadata: Some(pub3.clone()), + private_metadata: priv3.clone(), + public_metadata: pub3.clone(), royalty_info: None, serial_number: None, transferable: None, @@ -5428,10 +5428,10 @@ mod tests { assert!(token.unwrapped); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok3_key).unwrap(); - assert_eq!(priv_meta, priv3); + assert_eq!(priv_meta, priv3.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &tok3_key).unwrap(); - assert_eq!(pub_meta, pub3); + assert_eq!(pub_meta, pub3.unwrap()); // confirm the MyNFT2 metadata has been deleted from storage let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &tok2_key).unwrap(); @@ -6872,7 +6872,7 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("You are not authorized to perform this action on token MyNFT")); - let priv1 = Metadata { + let priv1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -6880,8 +6880,8 @@ mod tests { image: Some("privuri".to_string()), ..Extension::default() }), - }; - let pub1 = Metadata { + }); + let pub1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -6889,12 +6889,12 @@ mod tests { image: Some("puburi".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv1.clone()), - public_metadata: Some(pub1.clone()), + private_metadata: priv1.clone(), + public_metadata: pub1.clone(), royalty_info: None, serial_number: None, transferable: None, @@ -7164,10 +7164,10 @@ mod tests { // confirm the metadata is intact let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv1.clone()); + assert_eq!(priv_meta, priv1.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); // confirm the tx was logged to all involved parties let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); assert_eq!(total, 2); @@ -7256,10 +7256,10 @@ mod tests { // confirm the metadata is intact let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv1); + assert_eq!(priv_meta, priv1.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); - assert_eq!(pub_meta, pub1); + assert_eq!(pub_meta, pub1.unwrap()); // confirm the tx was logged to all involved parties let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 10).unwrap(); assert_eq!(total, 1); @@ -8589,7 +8589,7 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("You are not authorized to perform this action on token MyNFT")); - let priv1 = Metadata { + let priv1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -8597,8 +8597,8 @@ mod tests { image: Some("privuri".to_string()), ..Extension::default() }), - }; - let pub1 = Metadata { + }); + let pub1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("MyNFT".to_string()), @@ -8606,12 +8606,12 @@ mod tests { image: Some("puburi".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("MyNFT".to_string()), owner: Some("alice".to_string()), - private_metadata: Some(priv1.clone()), - public_metadata: Some(pub1.clone()), + private_metadata: priv1.clone(), + public_metadata: pub1.clone(), royalty_info: None, serial_number: None, transferable: None, @@ -8862,10 +8862,10 @@ mod tests { // confirm the metadata is intact let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv1.clone()); + assert_eq!(priv_meta, priv1.clone().unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); - assert_eq!(pub_meta, pub1.clone()); + assert_eq!(pub_meta, pub1.clone().unwrap()); // confirm the tx was logged to all involved parties let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); assert_eq!(total, 2); @@ -8978,10 +8978,10 @@ mod tests { // confirm the metadata is intact let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); - assert_eq!(priv_meta, priv1); + assert_eq!(priv_meta, priv1.unwrap()); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); - assert_eq!(pub_meta, pub1); + assert_eq!(pub_meta, pub1.unwrap()); // confirm the tx was logged to all involved parties let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 10).unwrap(); assert_eq!(total, 1); @@ -11523,7 +11523,7 @@ mod tests { let error = extract_error_msg(handle_result); assert!(error.contains("You do not own token NFT1")); - let pub1 = Metadata { + let pub1 = Some(Metadata { token_uri: None, extension: Some(Extension { name: Some("My1".to_string()), @@ -11531,11 +11531,11 @@ mod tests { image: Some("URI 1".to_string()), ..Extension::default() }), - }; + }); let execute_msg = ExecuteMsg::MintNft { token_id: Some("NFT1".to_string()), owner: Some("alice".to_string()), - public_metadata: Some(pub1.clone()), + public_metadata: pub1.clone(), private_metadata: None, royalty_info: None, serial_number: None, @@ -11682,7 +11682,7 @@ mod tests { assert!(token.unwrapped); let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); - assert_eq!(pub_meta, pub1); + assert_eq!(pub_meta, pub1.unwrap()); let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); assert!(priv_meta.is_none()); diff --git a/contracts/external/snip721-roles-impl/.cargo/config b/contracts/external/snip721-roles-impl/.cargo/config new file mode 100644 index 0000000..2a01f1d --- /dev/null +++ b/contracts/external/snip721-roles-impl/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/external/snip721-roles-impl/.gitignore b/contracts/external/snip721-roles-impl/.gitignore new file mode 100644 index 0000000..ea00b0b --- /dev/null +++ b/contracts/external/snip721-roles-impl/.gitignore @@ -0,0 +1,21 @@ +# Build results +/target +contract.wasm +contract.wasm.gz + +# Binaries +*.wasm +*.wasm.gz + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/external/snip721-roles-impl/Cargo.toml b/contracts/external/snip721-roles-impl/Cargo.toml new file mode 100644 index 0000000..9628222 --- /dev/null +++ b/contracts/external/snip721-roles-impl/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "snip721-roles-impl" +version = "1.0.0" +authors = ["bill wincer"] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = {workspace=true } +secret-toolkit = {workspace=true } +cosmwasm-storage = {workspace=true } +schemars = {workspace=true } +serde = {workspace=true } +bincode2 = "2.0.1" +base64 = "0.21.2" +primitive-types = { version = "0.12.2", default-features = false } +dao-snip721-extensions ={ workspace = true } +shade-protocol ={ workspace = true } +cosmwasm-schema ={ workspace = true } + + +[dev-dependencies] +cosmwasm-schema = { workspace = true } diff --git a/contracts/external/snip721-roles-impl/Makefile b/contracts/external/snip721-roles-impl/Makefile new file mode 100644 index 0000000..13c5cd1 --- /dev/null +++ b/contracts/external/snip721-roles-impl/Makefile @@ -0,0 +1,66 @@ +SECRETCLI = docker exec -it secretdev /usr/bin/secretcli + +.PHONY: all +all: clippy test + +.PHONY: check +check: + cargo check + +.PHONY: clippy +clippy: + cargo clippy + +.PHONY: test +test: unit-test integration-test + +.PHONY: unit-test +unit-test: + cargo test + +.PHONY: integration-test +integration-test: compile-optimized + cargo test --test '*' + +.PHONY: list-code +list-code: + $(SECRETCLI) query compute list-code + +.PHONY: compile _compile +compile: _compile contract.wasm.gz +_compile: + cargo build --target wasm32-unknown-unknown --locked + cp ./target/wasm32-unknown-unknown/debug/*.wasm ./contract.wasm + +.PHONY: compile-optimized _compile-optimized +compile-optimized: _compile-optimized contract.wasm.gz +_compile-optimized: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked + @# The following line is not necessary, may work only on linux (extra size optimization) + wasm-opt -Oz ./target/wasm32-unknown-unknown/release/*.wasm -o ./contract.wasm + +.PHONY: compile-optimized-reproducible +compile-optimized-reproducible: + docker run --rm -v "$$(pwd)":/contract \ + --mount type=volume,source="$$(basename "$$(pwd)")_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + enigmampc/secret-contract-optimizer:1.0.10 + +contract.wasm.gz: contract.wasm + cat ./contract.wasm | gzip -9 > ./contract.wasm.gz + +.PHONY: start-server +start-server: # CTRL+C to stop + docker run -it --rm \ + -p 26657:26657 -p 26656:26656 -p 1317:1317 \ + -v $$(pwd):/root/code \ + --name secretdev enigmampc/secret-network-sw-dev:latest + +.PHONY: schema +schema: + cargo run --example schema + +.PHONY: clean +clean: + cargo clean + rm -f ./contract.wasm ./contract.wasm.gz diff --git a/contracts/external/snip721-roles-impl/src/contract.rs b/contracts/external/snip721-roles-impl/src/contract.rs new file mode 100644 index 0000000..d9adaf7 --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/contract.rs @@ -0,0 +1,5202 @@ +/// This contract implements SNIP-721 standard: +/// https://github.com/SecretFoundation/SNIPs/blob/master/SNIP-721.md +use std::collections::HashSet; + +use base64::{engine::general_purpose, Engine as _}; +use cosmwasm_std::{ + attr, to_binary, Addr, Api, Binary, BlockInfo, CanonicalAddr, CosmosMsg, Deps, DepsMut, Env, + MessageInfo, Response, StdError, StdResult, Storage, WasmMsg, +}; +use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage}; +use primitive_types::U256; +use schemars::JsonSchema; +use secret_toolkit::{ + crypto::sha_256, + permit::{validate, Permit, RevokedPermits}, + utils::{pad_handle_result, pad_query_result}, + viewing_key::{ViewingKey, ViewingKeyStore}, +}; +use serde::{Deserialize, Serialize}; + +use crate::msg::{ + AccessLevel, BatchNftDossierElement, Burn, ContractStatus, Cw721Approval, Cw721OwnerOfResponse, + ExecuteAnswer, ExecuteMsg, InstantiateMsg, Mint, QueryAnswer, QueryMsg, QueryWithPermit, + ReceiverInfo, ResponseStatus::Success, Send, Snip721Approval, Transfer, ViewerInfo, +}; +use crate::receiver::{batch_receive_nft_msg, receive_nft_msg}; +use crate::royalties::{RoyaltyInfo, StoredRoyaltyInfo}; +use crate::state::{ + get_txs, json_may_load, json_save, load, may_load, remove, save, store_burn, store_mint, + store_transfer, AuthList, Config, Permission, PermissionType, ReceiveRegistration, CONFIG_KEY, + CREATOR_KEY, DEFAULT_ROYALTY_KEY, MINTERS_KEY, PREFIX_ALL_PERMISSIONS, PREFIX_AUTHLIST, + PREFIX_INFOS, PREFIX_MAP_TO_ID, PREFIX_MAP_TO_INDEX, PREFIX_MINT_RUN, PREFIX_MINT_RUN_NUM, + PREFIX_OWNER_PRIV, PREFIX_PRIV_META, PREFIX_PUB_META, PREFIX_RECEIVERS, PREFIX_REVOKED_PERMITS, + PREFIX_ROYALTY_INFO, VIEWING_KEY_ERR_MSG, +}; +use crate::token::{Metadata, Token}; +use crate::{ + expiration::Expiration, + msg::{InstantiateResponse, Minters, NftInfo, NumTokens, OwnerOf}, +}; +use crate::{ + inventory::{Inventory, InventoryIter}, + msg::ContractInfo, +}; +use crate::{ + mint_run::{SerialNumber, StoredMintRunInfo}, + state::Snip721Contract, +}; + +// enum used to return correct response from SetWhitelistedApproval +pub enum SetAppResp { + SetWhitelistedApproval, + ApproveAll, + RevokeAll, +} + +// table of bools used to alter AuthLists properly +#[derive(Default)] +pub struct AlterAuthTable { + // true if the specified token index should be added to an AuthList for that PermissionType + pub add: [bool; 3], + // true if all but the specified token index should be added to an AuthList for that PermType + pub full: [bool; 3], + // true if the specified token index should be removed from an AuthList for that PermType + pub remove: [bool; 3], + // true if the AuthList should be cleared for that Permission Type + pub clear: [bool; 3], + // true if there is at least one true in the table + pub has_update: bool, +} + +// table of bools used to alter a permission list appropriately +#[derive(Default)] +pub struct AlterPermTable { + // true if the address should be added to the permission list for that PermissionType + pub add: [bool; 3], + // true if the address should be removed from the permission list for that PermissionType + pub remove: [bool; 3], + // true if there is at least one true in the table + pub has_update: bool, +} + +// bundled info needed when setting accesses +pub struct ProcessAccInfo { + // the input token or a default + pub token: Token, + // input token's mint index or a default + pub idx: u32, + // true if there was an input token + pub token_given: bool, + // the accesses being set + pub accesses: [Option; 3], + // optional expiration + pub expires: Option, + // true if this is an operator trying to set permissions + pub from_oper: bool, +} + +// a receiver, their code hash, and whether they implement BatchReceiveNft +pub struct CacheReceiverInfo { + // the contract address + pub contract: CanonicalAddr, + // the contract's registration info + pub registration: ReceiveRegistration, +} + +// bundled info when prepping an authenticated token query +pub struct TokenQueryInfo { + // querier's address + viewer_raw: Option, + // error message String + err_msg: String, + // the requested token + token: Token, + // the requested token's index + idx: u32, + // true if the contract has public ownership + owner_is_public: bool, +} + +// permission type info +pub struct PermissionTypeInfo { + // index for view owner permission + pub view_owner_idx: usize, + // index for view private metadata permission + pub view_meta_idx: usize, + // index for transfer permission + pub transfer_idx: usize, + // number of permission types + pub num_types: usize, +} + +// an owner's inventory and the tokens they lost in this tx +pub struct InventoryUpdate { + // owner's inventory + pub inventory: Inventory, + // the list of lost tokens + pub remove: HashSet, +} +// list of tokens sent from one previous owner +pub struct SendFrom { + // the owner's address + pub owner: CanonicalAddr, + // the tokens that were sent + pub token_ids: Vec, +} +// used to cache owner information for dossier_list() +pub struct OwnerInfo { + // the owner's address + pub owner: CanonicalAddr, + // the view_owner privacy override + pub owner_is_public: bool, + // inventory approvals + pub inventory_approvals: Vec, + // expiration for global view_owner approval if applicable + pub view_owner_exp: Option, + // expiration for global view_private_metadata approval if applicable + pub view_meta_exp: Option, +} + +/// pad handle responses and log attributes to blocks of 256 bytes to prevent leaking info based on +/// response size +pub const BLOCK_SIZE: usize = 256; +/// max number of token ids to keep in id list block +pub const ID_BLOCK_SIZE: u32 = 64; + +////////////////////////////////////// Init /////////////////////////////////////// +/// Returns InitResult +/// +/// Initializes the contract +/// +/// # Arguments +/// +/// * `deps` - mutable reference to Extern containing all the contract's external dependencies +/// * `env` - Env of contract's environment +/// * `info` - contract execution info for authorization - identity of the call, and payment. +/// * `msg` - InitMsg passed in with the instantiation message + +impl + Snip721Contract +where + QueryExt: JsonSchema, + MetadataExt: Clone + Serialize + for<'a> Deserialize<'a>, +{ + pub fn instantiate( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + self.query_auth + .save(deps.storage, &msg.query_auth.into_valid(deps.api)?)?; + let creator_raw = deps.api.addr_canonicalize(info.sender.as_str())?; + save(deps.storage, CREATOR_KEY, &creator_raw)?; + let admin_raw = msg + .admin + .map(|a| { + deps.api + .addr_canonicalize(deps.api.addr_validate(&a)?.as_str()) + }) + .transpose()? + .unwrap_or(creator_raw); + let prng_seed = sha_256( + general_purpose::STANDARD + .encode(msg.entropy.as_str()) + .as_bytes(), + ); + ViewingKey::set_seed(deps.storage, &prng_seed); + + let init_config = msg.config.unwrap_or_default(); + + let config = Config { + name: msg.name, + symbol: msg.symbol, + admin: admin_raw.clone(), + mint_cnt: 0, + tx_cnt: 0, + token_cnt: 0, + status: ContractStatus::Normal.to_u8(), + token_supply_is_public: init_config.public_token_supply.unwrap_or(false), + owner_is_public: init_config.public_owner.unwrap_or(false), + sealed_metadata_is_enabled: init_config.enable_sealed_metadata.unwrap_or(false), + unwrap_to_private: init_config.unwrapped_metadata_is_private.unwrap_or(false), + minter_may_update_metadata: init_config.minter_may_update_metadata.unwrap_or(true), + owner_may_update_metadata: init_config.owner_may_update_metadata.unwrap_or(false), + burn_is_enabled: init_config.enable_burn.unwrap_or(false), + }; + + let minters = vec![admin_raw]; + save(deps.storage, CONFIG_KEY, &config)?; + save(deps.storage, MINTERS_KEY, &minters)?; + + if msg.royalty_info.is_some() { + self.store_royalties( + deps.storage, + deps.api, + msg.royalty_info.as_ref(), + None, + DEFAULT_ROYALTY_KEY, + )?; + } + + // perform the post init callback if needed + let messages: Vec = if let Some(callback) = msg.post_init_callback { + let execute = WasmMsg::Execute { + msg: callback.msg, + contract_addr: callback.contract_address, + code_hash: callback.code_hash, + funds: callback.send, + }; + vec![execute.into()] + } else { + Vec::new() + }; + Ok(Response::new() + .add_messages(messages) + .set_data(to_binary(&InstantiateResponse { + contract_address: env.contract.address, + code_hash: env.contract.code_hash, + })?)) + } + + ///////////////////////////////////// Handle ////////////////////////////////////// + /// Returns StdResult + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - Env of contract's environment + /// * `info` - contract execution info for authorization - identity of the call, and payment. + /// * `msg` - HandleMsg passed in with the execute message + pub fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> StdResult { + let mut config: Config = load(deps.storage, CONFIG_KEY)?; + + let response = match msg { + ExecuteMsg::MintNft { + token_id, + owner, + public_metadata, + private_metadata, + serial_number, + royalty_info, + transferable, + memo, + extension, + .. + } => self.mint( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + token_id, + owner, + public_metadata, + private_metadata, + serial_number, + royalty_info, + transferable, + memo, + extension, + ), + ExecuteMsg::BatchMintNft { mints, .. } => self.batch_mint( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + mints, + ), + ExecuteMsg::MintNftClones { + mint_run_id, + quantity, + owner, + public_metadata, + private_metadata, + royalty_info, + memo, + extension, + .. + } => self.mint_clones( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + mint_run_id.as_ref(), + quantity, + owner, + public_metadata, + private_metadata, + royalty_info, + memo, + extension, + ), + ExecuteMsg::SetMetadata { + token_id, + public_metadata, + private_metadata, + .. + } => self.set_metadata( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &token_id, + public_metadata, + private_metadata, + ), + ExecuteMsg::SetRoyaltyInfo { + token_id, + royalty_info, + .. + } => self.set_royalty_info( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + token_id.as_deref(), + royalty_info.as_ref(), + ), + ExecuteMsg::Reveal { token_id, .. } => self.reveal( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &token_id, + ), + ExecuteMsg::MakeOwnershipPrivate { .. } => self.make_owner_private( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + ), + ExecuteMsg::SetGlobalApproval { + token_id, + view_owner, + view_private_metadata, + expires, + .. + } => self.set_global_approval( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + token_id, + view_owner, + view_private_metadata, + expires, + ), + ExecuteMsg::SetWhitelistedApproval { + address, + token_id, + view_owner, + view_private_metadata, + transfer, + expires, + .. + } => self.set_whitelisted_approval( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &address, + token_id, + view_owner, + view_private_metadata, + transfer, + expires, + SetAppResp::SetWhitelistedApproval, + ), + ExecuteMsg::Approve { + spender, + token_id, + expires, + .. + } => self.approve_revoke( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &spender, + &token_id, + expires, + true, + ), + ExecuteMsg::Revoke { + spender, token_id, .. + } => self.approve_revoke( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &spender, + &token_id, + None, + false, + ), + ExecuteMsg::ApproveAll { + operator, expires, .. + } => self.set_whitelisted_approval( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &operator, + None, + None, + None, + Some(AccessLevel::All), + expires, + SetAppResp::ApproveAll, + ), + ExecuteMsg::RevokeAll { operator, .. } => self.set_whitelisted_approval( + deps, + &env, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &operator, + None, + None, + None, + Some(AccessLevel::None), + None, + SetAppResp::RevokeAll, + ), + ExecuteMsg::TransferNft { + recipient, + token_id, + memo, + .. + } => self.transfer_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + recipient, + token_id, + memo, + ), + ExecuteMsg::BatchTransferNft { transfers, .. } => self.batch_transfer_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + transfers, + ), + ExecuteMsg::SendNft { + contract, + receiver_info, + token_id, + msg, + memo, + .. + } => self.send_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + contract, + receiver_info, + token_id, + msg, + memo, + ), + ExecuteMsg::BatchSendNft { sends, .. } => self.batch_send_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + sends, + ), + ExecuteMsg::RegisterReceiveNft { + code_hash, + also_implements_batch_receive_nft, + .. + } => self.register_receive_nft( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + code_hash, + also_implements_batch_receive_nft, + ), + ExecuteMsg::BurnNft { token_id, memo, .. } => self.burn_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + token_id, + memo, + ), + ExecuteMsg::BatchBurnNft { burns, .. } => self.batch_burn_nft( + deps, + &env, + &info.sender, + &mut config, + ContractStatus::Normal.to_u8(), + burns, + ), + ExecuteMsg::CreateViewingKey { entropy, .. } => self.create_key( + deps, + &env, + &info, + &config, + ContractStatus::StopTransactions.to_u8(), + &entropy, + ), + ExecuteMsg::SetViewingKey { key, .. } => self.set_key( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + key, + ), + ExecuteMsg::AddMinters { minters, .. } => self.add_minters( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &minters, + ), + ExecuteMsg::RemoveMinters { minters, .. } => self.remove_minters( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &minters, + ), + ExecuteMsg::SetMinters { minters, .. } => self.set_minters( + deps, + &info.sender, + &config, + ContractStatus::StopTransactions.to_u8(), + &minters, + ), + ExecuteMsg::ChangeAdmin { address, .. } => self.change_admin( + deps, + &info.sender, + &mut config, + ContractStatus::StopTransactions.to_u8(), + &address, + ), + ExecuteMsg::SetContractStatus { level, .. } => { + self.set_contract_status(deps, &info.sender, &mut config, level) + } + ExecuteMsg::RevokePermit { permit_name, .. } => { + self.revoke_permit(deps.storage, &info.sender, &permit_name) + } + ExecuteMsg::Extension { msg: _ } => Ok(Response::default()), + }; + pad_handle_result(response, BLOCK_SIZE) + } + + /// Returns StdResult + /// + /// mint a new token + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `token_id` - optional token id, if not specified, use token index + /// * `owner` - optional owner of this token, if not specified, use the minter's address + /// * `public_metadata` - optional public metadata viewable by everyone + /// * `private_metadata` - optional private metadata viewable only by owner and whitelist + /// * `serial_number` - optional serial number information for this token + /// * `royalty_info` - optional royalties information for this token + /// * `transferable` - optionally true if this token is transferable + /// * `memo` - optional memo for the mint tx + #[allow(clippy::too_many_arguments)] + pub fn mint( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + token_id: Option, + owner: Option, + public_metadata: Option, + private_metadata: Option, + serial_number: Option, + royalty_info: Option, + transferable: Option, + memo: Option, + extension: MetadataExt, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let minters: Vec = may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + if !minters.contains(&sender_raw) { + return Err(StdError::generic_err( + "Only designated minters are allowed to mint", + )); + } + let mints = vec![Mint { + token_id, + owner, + public_metadata, + private_metadata, + serial_number, + royalty_info, + transferable, + memo, + extension, + }]; + let mut minted = self.mint_list(deps, env, config, &sender_raw, mints)?; + let minted_str = minted.pop().unwrap_or_default(); + Ok(Response::new() + .add_attributes(vec![attr("minted", &minted_str)]) + .set_data(to_binary(&ExecuteAnswer::MintNft { + token_id: minted_str, + })?)) + } + + /// Returns StdResult + /// + /// mints many tokens + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `mints` - the list of mints to perform + pub fn batch_mint( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + mints: Vec>, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let minters: Vec = may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + if !minters.contains(&sender_raw) { + return Err(StdError::generic_err( + "Only designated minters are allowed to mint", + )); + } + let minted = self.mint_list(deps, env, config, &sender_raw, mints)?; + Ok(Response::new() + .add_attributes(vec![attr("minted", format!("{:?}", &minted))]) + .set_data(to_binary(&ExecuteAnswer::BatchMintNft { + token_ids: minted, + })?)) + } + + /// Returns StdResult + /// + /// mints clones of a token + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `mint_run_id` - optional id used to track subsequent mint runs + /// * `quantity` - number of clones to mint + /// * `owner` - optional owner of this token, if not specified, use the minter's address + /// * `public_metadata` - optional public metadata viewable by everyone + /// * `private_metadata` - optional private metadata viewable only by owner and whitelist + /// * `royalty_info` - optional royalties information for these clones + /// * `memo` - optional memo for the mint txs + #[allow(clippy::too_many_arguments)] + pub fn mint_clones( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + mint_run_id: Option<&String>, + quantity: u32, + owner: Option, + public_metadata: Option, + private_metadata: Option, + royalty_info: Option, + memo: Option, + extension: MetadataExt, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let minters: Vec = may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + if !minters.contains(&sender_raw) { + return Err(StdError::generic_err( + "Only designated minters are allowed to mint", + )); + } + if quantity == 0 { + return Err(StdError::generic_err("Quantity can not be zero")); + } + let mint_run = mint_run_id + .map(|i| { + let key = i.as_bytes(); + let mut run_store = PrefixedStorage::new(deps.storage, PREFIX_MINT_RUN_NUM); + let last_num: u32 = may_load(&run_store, key)?.unwrap_or(0); + let this_num: u32 = last_num.checked_add(1).ok_or_else(|| { + StdError::generic_err(format!( + "Mint run ID {} has already reached its maximum possible value", + i + )) + })?; + save(&mut run_store, key, &this_num)?; + Ok::(this_num) + }) + .transpose()?; + let mut serial_number = SerialNumber { + mint_run, + serial_number: 1, + quantity_minted_this_run: Some(quantity), + }; + let mut mints: Vec> = Vec::new(); + for _ in 0..quantity { + mints.push(Mint { + token_id: None, + owner: owner.clone(), + public_metadata: public_metadata.clone(), + private_metadata: private_metadata.clone(), + serial_number: Some(serial_number.clone()), + royalty_info: royalty_info.clone(), + transferable: Some(true), + memo: memo.clone(), + extension: extension.clone(), + }); + serial_number.serial_number += 1; + } + let mut minted = self.mint_list(deps, env, config, &sender_raw, mints)?; + // if mint_list did not error, there must be at least one token id + let first_minted = minted + .first() + .ok_or_else(|| StdError::generic_err("List of minted tokens is empty"))? + .clone(); + let last_minted = minted + .pop() + .ok_or_else(|| StdError::generic_err("List of minted tokens is empty"))?; + + Ok(Response::new() + .add_attributes(vec![ + attr("first_minted", &first_minted), + attr("last_minted", &last_minted), + ]) + .set_data(to_binary(&ExecuteAnswer::MintNftClones { + first_minted, + last_minted, + })?)) + } + + /// Returns StdResult + /// + /// sets new public and/or private metadata + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `token_id` - token id String slice of token whose metadata should be updated + /// * `public_metadata` - the optional new public metadata viewable by everyone + /// * `private_metadata` - the optional new private metadata viewable by everyone + #[allow(clippy::too_many_arguments)] + pub fn set_metadata( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + token_id: &str, + public_metadata: Option, + private_metadata: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let custom_err = format!("Not authorized to update metadata of token {}", token_id); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + let (token, idx) = self.get_token(deps.storage, token_id, opt_err)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if !(token.owner == sender_raw && config.owner_may_update_metadata) { + let minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + if !(minters.contains(&sender_raw) && config.minter_may_update_metadata) { + return Err(StdError::generic_err(custom_err)); + } + } + if let Some(public) = public_metadata { + self.set_metadata_impl(deps.storage, &token, idx, PREFIX_PUB_META, &public)?; + } + if let Some(private) = private_metadata { + self.set_metadata_impl(deps.storage, &token, idx, PREFIX_PRIV_META, &private)?; + } + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMetadata { status: Success })?)) + } + + /// Returns StdResult + /// + /// sets new royalty information for a specified token or if no token ID is provided, sets new + /// royalty information as the contract's default + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `token_id` - optional token id String slice of token whose royalty info should be updated + /// * `royalty_info` - a optional reference to the new RoyaltyInfo + pub fn set_royalty_info( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + token_id: Option<&str>, + royalty_info: Option<&RoyaltyInfo>, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + // set a token's royalties + if let Some(id) = token_id { + let custom_err = "A token's RoyaltyInfo may only be set by the token creator when they are also the token owner"; + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(custom_err) + }; + let (token, idx) = self.get_token(deps.storage, id, opt_err)?; + if !token.transferable { + return Err(StdError::generic_err( + "Non-transferable tokens can not be sold, so royalties are meaningless", + )); + } + let token_key = idx.to_le_bytes(); + let run_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MINT_RUN); + let mint_run: StoredMintRunInfo = load(&run_store, &token_key)?; + if sender_raw != mint_run.token_creator || sender_raw != token.owner { + return Err(StdError::generic_err(custom_err)); + } + let default_roy = royalty_info.as_ref().map_or_else( + || may_load::(deps.storage, DEFAULT_ROYALTY_KEY), + |_r| Ok(None), + )?; + let mut roy_store = PrefixedStorage::new(deps.storage, PREFIX_ROYALTY_INFO); + self.store_royalties( + &mut roy_store, + deps.api, + royalty_info, + default_roy.as_ref(), + &token_key, + )?; + // set default royalty + } else { + let minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + if !minters.contains(&sender_raw) { + return Err(StdError::generic_err( + "Only designated minters can set default royalties for the contract", + )); + } + self.store_royalties( + deps.storage, + deps.api, + royalty_info, + None, + DEFAULT_ROYALTY_KEY, + )?; + }; + + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetRoyaltyInfo { + status: Success, + })?), + ) + } + + /// Returns StdResult + /// + /// makes the sealed private metadata public + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `token_id` - token id String slice of token whose metadata should be updated + pub fn reveal( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + token_id: &str, + ) -> StdResult { + self.check_status(config.status, priority)?; + if !config.sealed_metadata_is_enabled { + return Err(StdError::generic_err( + "Sealed metadata functionality is not enabled for this contract", + )); + } + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let custom_err = format!("You do not own token {}", token_id); + // if token supply is private, don't leak that the token id does not exist + // instead just say they do not own that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + let (mut token, idx) = self.get_token(deps.storage, token_id, opt_err)?; + if token.unwrapped { + return Err(StdError::generic_err( + "This token has already been unwrapped", + )); + } + if token.owner != sender_raw { + return Err(StdError::generic_err(custom_err)); + } + token.unwrapped = true; + let token_key = idx.to_le_bytes(); + let mut info_store = PrefixedStorage::new(deps.storage, PREFIX_INFOS); + json_save(&mut info_store, &token_key, &token)?; + if !config.unwrap_to_private { + let mut priv_store = PrefixedStorage::new(deps.storage, PREFIX_PRIV_META); + let may_priv: Option = may_load(&priv_store, &token_key)?; + if let Some(metadata) = may_priv { + remove(&mut priv_store, &token_key); + let mut pub_store = PrefixedStorage::new(deps.storage, PREFIX_PUB_META); + save(&mut pub_store, &token_key, &metadata)?; + } + } + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::Reveal { status: Success })?)) + } + + /// Returns StdResult + /// + /// grants/revokes trasfer permission on a token + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `spender` - a reference to the address being granted permission + /// * `token_id` - string slice of the token id to grant permission to + /// * `expires` - optional Expiration for this approval + /// * `is_approve` - true if this is an Approve call + #[allow(clippy::too_many_arguments)] + pub fn approve_revoke( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &Config, + priority: u8, + spender: &str, + token_id: &str, + expires: Option, + is_approve: bool, + ) -> StdResult { + self.check_status(config.status, priority)?; + let address_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(spender)?.as_str())?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let custom_err = format!( + "Not authorized to grant/revoke transfer permission for token {}", + token_id + ); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + let (token, idx) = self.get_token(deps.storage, token_id, opt_err)?; + let mut all_perm: Option> = None; + let mut from_oper = false; + let transfer_idx = PermissionType::Transfer.to_usize(); + // if not called by the owner, check if message sender has operator status + if token.owner != sender_raw { + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let may_list: Option> = + json_may_load(&all_store, token.owner.as_slice())?; + if let Some(list) = may_list.clone() { + if let Some(perm) = list.iter().find(|&p| p.address == sender_raw) { + if let Some(exp) = perm.expirations[transfer_idx] { + if exp.is_expired(&env.block) { + return Err(StdError::generic_err(format!( + "Transfer authority for all tokens of {} has expired", + &deps.api.addr_humanize(&token.owner)? + ))); + } else { + from_oper = true; + } + } + } + } + if !from_oper { + return Err(StdError::generic_err(custom_err)); + } + all_perm = may_list; + } + let mut accesses: [Option; 3] = [None, None, None]; + let response: ExecuteAnswer; + if is_approve { + accesses[transfer_idx] = Some(AccessLevel::ApproveToken); + response = ExecuteAnswer::Approve { status: Success }; + } else { + accesses[transfer_idx] = Some(AccessLevel::RevokeToken); + response = ExecuteAnswer::Revoke { status: Success }; + } + let owner = token.owner.clone(); + let mut proc_info = ProcessAccInfo { + token, + idx, + token_given: true, + accesses, + expires, + from_oper, + }; + self.process_accesses( + deps.storage, + env, + &address_raw, + &owner, + &mut proc_info, + all_perm, + )?; + let res = Response::new().set_data(to_binary(&response)?); + Ok(res) + } + + /// Returns StdResult + /// + /// makes an address' token ownership private + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + pub fn make_owner_private( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + // only need to do this if the contract has public ownership + if config.owner_is_public { + let mut priv_store = PrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + save(&mut priv_store, sender_raw.as_slice(), &false)? + } + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::MakeOwnershipPrivate { + status: Success, + })?), + ) + } + + /// Returns StdResult + /// + /// adds/revokes access for everyone + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `token_id` - optional token id to apply approvals to + /// * `view_owner` - optional access level for viewing token ownership + /// * `view_private_metadata` - optional access level for viewing private metadata + /// * `expires` - optional Expiration for this approval + #[allow(clippy::too_many_arguments)] + pub fn set_global_approval( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &Config, + priority: u8, + token_id: Option, + view_owner: Option, + view_private_metadata: Option, + expires: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let token_given: bool; + // use this "address" to represent global permission + let global_raw = CanonicalAddr(Binary::from(b"public")); + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let mut custom_err = String::new(); + let (token, idx) = if let Some(id) = token_id { + token_given = true; + custom_err = format!("You do not own token {}", id); + // if token supply is private, don't leak that the token id does not exist + // instead just say they do not own that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + self.get_token(deps.storage, &id, opt_err)? + } else { + token_given = false; + ( + Token { + owner: sender_raw.clone(), + permissions: Vec::new(), + unwrapped: false, + transferable: true, + }, + 0, + ) + }; + // if trying to set token permissions when you are not the owner + if token_given && token.owner != sender_raw { + return Err(StdError::generic_err(custom_err)); + } + let mut accesses: [Option; 3] = [None, None, None]; + accesses[PermissionType::ViewOwner.to_usize()] = view_owner; + accesses[PermissionType::ViewMetadata.to_usize()] = view_private_metadata; + let mut proc_info = ProcessAccInfo { + token, + idx, + token_given, + accesses, + expires, + from_oper: false, + }; + self.process_accesses( + deps.storage, + env, + &global_raw, + &sender_raw, + &mut proc_info, + None, + )?; + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetGlobalApproval { + status: Success, + })?), + ) + } + + /// Returns StdResult + /// + /// sets specified permissions for an address + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest status level this action is permitted at + /// * `address` - a reference to the address being granted permission + /// * `token_id` - optional token id to apply approvals to + /// * `view_owner` - optional access level for viewing token ownership + /// * `view_private_metadata` - optional access level for viewing private metadata + /// * `transfer` - optional access level for transferring tokens + /// * `expires` - optional Expiration for this approval + /// * `response_type` - which response to return for SetWhitelistedApproval, ApproveAll, or RevokeAll + #[allow(clippy::too_many_arguments)] + pub fn set_whitelisted_approval( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &Config, + priority: u8, + address: &str, + token_id: Option, + view_owner: Option, + view_private_metadata: Option, + transfer: Option, + expires: Option, + response_type: SetAppResp, + ) -> StdResult { + self.check_status(config.status, priority)?; + let token_given: bool; + let address_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(address)?.as_str())?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let mut custom_err = String::new(); + let (token, idx) = if let Some(id) = token_id { + token_given = true; + custom_err = format!("You do not own token {}", id); + // if token supply is private, don't leak that the token id does not exist + // instead just say they do not own that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + self.get_token(deps.storage, &id, opt_err)? + } else { + token_given = false; + ( + Token { + owner: sender_raw.clone(), + permissions: Vec::new(), + unwrapped: false, + transferable: true, + }, + 0, + ) + }; + // if trying to set token permissions when you are not the owner + if token_given && token.owner != sender_raw { + return Err(StdError::generic_err(custom_err)); + } + let mut accesses: [Option; 3] = [None, None, None]; + accesses[PermissionType::ViewOwner.to_usize()] = view_owner; + accesses[PermissionType::ViewMetadata.to_usize()] = view_private_metadata; + accesses[PermissionType::Transfer.to_usize()] = transfer; + let mut proc_info = ProcessAccInfo { + token, + idx, + token_given, + accesses, + expires, + from_oper: false, + }; + self.process_accesses( + deps.storage, + env, + &address_raw, + &sender_raw, + &mut proc_info, + None, + )?; + let response = match response_type { + SetAppResp::SetWhitelistedApproval => { + ExecuteAnswer::SetWhitelistedApproval { status: Success } + } + SetAppResp::ApproveAll => ExecuteAnswer::ApproveAll { status: Success }, + SetAppResp::RevokeAll => ExecuteAnswer::RevokeAll { status: Success }, + }; + let res = Response::new().set_data(to_binary(&response)?); + Ok(res) + } + + /// Returns StdResult + /// + /// burns many tokens + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `burns` - the list of burns to perform + pub fn batch_burn_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + burns: Vec, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + self.burn_list(deps, &env.block, config, &sender_raw, burns)?; + let res = + Response::new().set_data(to_binary(&ExecuteAnswer::BatchBurnNft { status: Success })?); + Ok(res) + } + + /// Returns StdResult + /// + /// burns a token + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `token_id` - token id String of token to be burnt + /// * `memo` - optional memo for the burn tx + #[allow(clippy::too_many_arguments)] + fn burn_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + token_id: String, + memo: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let burns = vec![Burn { + token_ids: vec![token_id], + memo, + }]; + self.burn_list(deps, &env.block, config, &sender_raw, burns)?; + let res = Response::new().set_data(to_binary(&ExecuteAnswer::BurnNft { status: Success })?); + Ok(res) + } + + /// Returns StdResult + /// + /// transfer many tokens + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `transfers` - list of transfers to perform + pub fn batch_transfer_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + transfers: Vec, + ) -> StdResult { + self.check_status(config.status, priority)?; + let _m = self.send_list(deps, env, sender, config, Some(transfers), None)?; + + let res = Response::new().set_data(to_binary(&ExecuteAnswer::BatchTransferNft { + status: Success, + })?); + Ok(res) + } + + /// Returns StdResult + /// + /// transfer a token + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `recipient` - the address receiving the token + /// * `token_id` - token id String of token to be transferred + /// * `memo` - optional memo for the mint tx + #[allow(clippy::too_many_arguments)] + pub fn transfer_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + recipient: String, + token_id: String, + memo: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let transfers = Some(vec![Transfer { + recipient, + token_ids: vec![token_id], + memo, + }]); + let _m = self.send_list(deps, env, sender, config, transfers, None)?; + + let res = + Response::new().set_data(to_binary(&ExecuteAnswer::TransferNft { status: Success })?); + Ok(res) + } + + /// Returns StdResult + /// + /// sends tokens to contracts, and calls those contracts' ReceiveNft. Will error if any + /// contract has not registered its ReceiveNft + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `sends` - list of SendNfts to perform + fn batch_send_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + sends: Vec, + ) -> StdResult { + self.check_status(config.status, priority)?; + let messages = self.send_list(deps, env, sender, config, None, Some(sends))?; + + let res = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::BatchSendNft { status: Success })?); + Ok(res) + } + + /// Returns StdResult + /// + /// sends a token to a contract, and calls that contract's ReceiveNft. Will error if the + /// contract has not registered its ReceiveNft + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `contract` - the address of the contract receiving the token + /// * `receiver_info` - optional code hash and BatchReceiveNft implementation status of + /// the recipient contract + /// * `token_id` - ID String of the token that was sent + /// * `msg` - optional msg used to control ReceiveNft logic + /// * `memo` - optional memo for the mint tx + #[allow(clippy::too_many_arguments)] + fn send_nft( + &self, + deps: DepsMut, + env: &Env, + sender: &Addr, + config: &mut Config, + priority: u8, + contract: String, + receiver_info: Option, + token_id: String, + msg: Option, + memo: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sends = Some(vec![Send { + contract, + receiver_info, + token_ids: vec![token_id], + msg, + memo, + }]); + let messages = self.send_list(deps, env, sender, config, None, sends)?; + + let res = Response::new() + .add_messages(messages) + .set_data(to_binary(&ExecuteAnswer::SendNft { status: Success })?); + Ok(res) + } + + /// Returns StdResult + /// + /// registers a contract's ReceiveNft + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `code_hash` - code hash String of the registering contract + /// * `impl_batch` - optionally true if the contract also implements BatchReceiveNft + pub fn register_receive_nft( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + code_hash: String, + impl_batch: Option, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + let regrec = ReceiveRegistration { + code_hash, + impl_batch: impl_batch.unwrap_or(false), + }; + let mut store = PrefixedStorage::new(deps.storage, PREFIX_RECEIVERS); + save(&mut store, sender_raw.as_slice(), ®rec)?; + let res = Response::new().set_data(to_binary(&ExecuteAnswer::RegisterReceiveNft { + status: Success, + })?); + Ok(res) + } + + /// Returns StdResult + /// + /// creates a viewing key + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `info` - contract execution info for authorization - identity of the call, and payment. + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `entropy` - string slice of the input String to be used as entropy in randomization + pub fn create_key( + &self, + deps: DepsMut, + env: &Env, + info: &MessageInfo, + config: &Config, + priority: u8, + entropy: &str, + ) -> StdResult { + self.check_status(config.status, priority)?; + let key = ViewingKey::create( + deps.storage, + info, + env, + info.sender.as_str(), + entropy.as_ref(), + ); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ViewingKey { key })?)) + } + + /// Returns StdResult + /// + /// sets the viewing key to the input String + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `key` - String to be used as the viewing key + pub fn set_key( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + key: String, + ) -> StdResult { + self.check_status(config.status, priority)?; + ViewingKey::set(deps.storage, sender.as_str(), &key); + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ViewingKey { key })?)) + } + + /// Returns StdResult + /// + /// add a list of minters + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `new_minters` - list of minter addresses to add + pub fn add_minters( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + new_minters: &[String], + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if config.admin != sender_raw { + return Err(StdError::generic_err( + "This is an admin command and can only be run from the admin address", + )); + } + let mut minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + let mut update = false; + for minter in new_minters { + let minter_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(minter)?.as_str())?; + if !minters.contains(&minter_raw) { + minters.push(minter_raw); + update = true; + } + } + // only save if the list changed + if update { + save(deps.storage, MINTERS_KEY, &minters)?; + } + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::AddMinters { status: Success })?)) + } + + /// Returns StdResult + /// + /// remove a list of minters + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `no_minters` - list of minter addresses to remove + pub fn remove_minters( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + no_minters: &[String], + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if config.admin != sender_raw { + return Err(StdError::generic_err( + "This is an admin command and can only be run from the admin address", + )); + } + let may_minters: Option> = may_load(deps.storage, MINTERS_KEY)?; + if let Some(mut minters) = may_minters { + let old_len = minters.len(); + let no_raw: Vec = no_minters + .iter() + .map(|x| { + deps.api + .addr_canonicalize(deps.api.addr_validate(x)?.as_str()) + }) + .collect::>>()?; + minters.retain(|m| !no_raw.contains(m)); + let new_len = minters.len(); + if new_len > 0 { + if old_len != new_len { + save(deps.storage, MINTERS_KEY, &minters)?; + } + } else { + remove(deps.storage, MINTERS_KEY); + } + } + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::RemoveMinters { + status: Success, + })?), + ) + } + + /// Returns StdResult + /// + /// define the exact list of minters + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `human_minters` - exact list of minter addresses + pub fn set_minters( + &self, + deps: DepsMut, + sender: &Addr, + config: &Config, + priority: u8, + human_minters: &[String], + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if config.admin != sender_raw { + return Err(StdError::generic_err( + "This is an admin command and can only be run from the admin address", + )); + } + // remove duplicates from the minters list + let minters_raw: Vec = human_minters + .iter() + .map(|x| { + deps.api + .addr_canonicalize(deps.api.addr_validate(x)?.as_str()) + }) + .collect::>>()?; + let mut sortable: Vec<&[u8]> = minters_raw.iter().map(|x| x.as_slice()).collect(); + sortable.sort_unstable(); + sortable.dedup(); + let minters: Vec = sortable + .iter() + .map(|x| CanonicalAddr(Binary(x.to_vec()))) + .collect(); + if minters.is_empty() { + remove(deps.storage, MINTERS_KEY); + } else { + save(deps.storage, MINTERS_KEY, &minters)?; + } + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::SetMinters { status: Success })?)) + } + + /// Returns StdResult + /// + /// change the admin address + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `priority` - u8 representation of highest ContractStatus level this action is permitted + /// * `address` - new admin address + pub fn change_admin( + &self, + deps: DepsMut, + sender: &Addr, + config: &mut Config, + priority: u8, + address: &str, + ) -> StdResult { + self.check_status(config.status, priority)?; + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if config.admin != sender_raw { + return Err(StdError::generic_err( + "This is an admin command and can only be run from the admin address", + )); + } + let new_admin = deps + .api + .addr_canonicalize(deps.api.addr_validate(address)?.as_str())?; + if new_admin != config.admin { + config.admin = new_admin; + save(deps.storage, CONFIG_KEY, &config)?; + } + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::ChangeAdmin { status: Success })?)) + } + + /// Returns StdResult + /// + /// set the contract status level + /// + /// # Arguments + /// + /// * `deps` - mutable reference to Extern containing all the contract's external dependencies + /// * `sender` - a reference to the message sender address + /// * `config` - a mutable reference to the Config + /// * `level` - new ContractStatus + pub fn set_contract_status( + &self, + deps: DepsMut, + sender: &Addr, + config: &mut Config, + level: ContractStatus, + ) -> StdResult { + let sender_raw = deps.api.addr_canonicalize(sender.as_str())?; + if config.admin != sender_raw { + return Err(StdError::generic_err( + "This is an admin command and can only be run from the admin address", + )); + } + let new_status = level.to_u8(); + if config.status != new_status { + config.status = new_status; + save(deps.storage, CONFIG_KEY, &config)?; + } + Ok( + Response::new().set_data(to_binary(&ExecuteAnswer::SetContractStatus { + status: Success, + })?), + ) + } + + /// Returns StdResult + /// + /// revoke the ability to use a specified permit + /// + /// # Arguments + /// + /// * `storage` - mutable reference to the contract's storage + /// * `sender` - a reference to the message sender address + /// * `permit_name` - string slice of the name of the permit to revoke + fn revoke_permit( + &self, + storage: &mut dyn Storage, + sender: &Addr, + permit_name: &str, + ) -> StdResult { + RevokedPermits::revoke_permit( + storage, + PREFIX_REVOKED_PERMITS, + sender.as_str(), + permit_name, + ); + + Ok(Response::new().set_data(to_binary(&ExecuteAnswer::RevokePermit { status: Success })?)) + } + + /////////////////////////////////////// Query ///////////////////////////////////// + /// Returns StdResult + /// + /// # Arguments + /// + /// * `deps` - reference to Extern containing all the contract's external dependencies + /// * `env` - Env of contract's environment + /// * `msg` - QueryMsg passed in with the query call + pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let response = match msg { + QueryMsg::ContractInfo {} => self.query_contract_info(deps.storage), + QueryMsg::ContractCreator {} => self.query_contract_creator(deps), + QueryMsg::RoyaltyInfo { token_id, viewer } => { + self.query_royalty(deps, &env.block, token_id.as_deref(), viewer, None) + } + QueryMsg::ContractConfig {} => self.query_config(deps.storage), + QueryMsg::Minters {} => self.query_minters(deps), + QueryMsg::NumTokens { viewer } => self.query_num_tokens(deps, viewer, None), + QueryMsg::AllTokens { + viewer, + start_after, + limit, + } => self.query_all_tokens(deps, viewer, start_after.as_deref(), limit, None), + QueryMsg::OwnerOf { + token_id, + viewer, + include_expired, + } => self.query_owner_of(deps, &env.block, &token_id, viewer, include_expired, None), + QueryMsg::NftInfo { token_id } => self.query_nft_info(deps.storage, &token_id), + QueryMsg::PrivateMetadata { token_id, viewer } => { + self.query_private_meta(deps, &env.block, &token_id, viewer, None) + } + QueryMsg::AllNftInfo { + token_id, + viewer, + include_expired, + } => { + self.query_all_nft_info(deps, &env.block, &token_id, viewer, include_expired, None) + } + QueryMsg::NftDossier { + token_id, + viewer, + include_expired, + } => self.query_nft_dossier(deps, &env.block, token_id, viewer, include_expired, None), + QueryMsg::BatchNftDossier { + token_ids, + viewer, + include_expired, + } => self.query_batch_nft_dossier( + deps, + &env.block, + token_ids, + viewer, + include_expired, + None, + ), + QueryMsg::TokenApprovals { + token_id, + viewing_key, + include_expired, + } => self.query_token_approvals( + deps, + &env.block, + &token_id, + Some(&viewing_key), + include_expired, + None, + ), + QueryMsg::InventoryApprovals { + address, + viewing_key, + include_expired, + } => { + let viewer = Some(ViewerInfo { + address, + viewing_key, + }); + self.query_inventory_approvals(deps, &env.block, viewer, include_expired, None) + } + QueryMsg::ApprovedForAll { + owner, + viewing_key, + include_expired, + } => self.query_approved_for_all( + deps, + &env.block, + Some(&owner), + viewing_key.as_deref(), + include_expired, + None, + ), + QueryMsg::Tokens { + owner, + viewer, + viewing_key, + start_after, + limit, + } => self.query_tokens( + deps, + &env.block, + &owner, + viewer.as_deref(), + viewing_key.as_deref(), + start_after.as_deref(), + limit, + None, + ), + QueryMsg::NumTokensOfOwner { + owner, + viewer, + viewing_key, + } => self.query_num_owner_tokens( + deps, + &env.block, + &owner, + viewer.as_deref(), + viewing_key.as_deref(), + None, + ), + QueryMsg::VerifyTransferApproval { + token_ids, + address, + viewing_key, + } => { + let viewer = Some(ViewerInfo { + address, + viewing_key, + }); + self.query_verify_approval(deps, &env.block, token_ids, viewer, None) + } + QueryMsg::IsUnwrapped { token_id } => self.query_is_unwrapped(deps.storage, &token_id), + QueryMsg::IsTransferable { token_id } => { + self.query_is_transferable(deps.storage, &token_id) + } + QueryMsg::ImplementsNonTransferableTokens {} => { + to_binary(&QueryAnswer::ImplementsNonTransferableTokens { is_enabled: true }) + } + QueryMsg::ImplementsTokenSubtype {} => { + to_binary(&QueryAnswer::ImplementsTokenSubtype { is_enabled: true }) + } + QueryMsg::TransactionHistory { + address, + viewing_key, + page, + page_size, + } => { + let viewer = Some(ViewerInfo { + address, + viewing_key, + }); + self.query_transactions(deps, viewer, page, page_size, None) + } + QueryMsg::RegisteredCodeHash { contract } => self.query_code_hash(deps, &contract), + QueryMsg::WithPermit { permit, query } => { + self.permit_queries(deps, &env, permit, query) + } + QueryMsg::QueryExtension { .. } => Ok(Binary::default()), + }; + pad_query_result(response, BLOCK_SIZE) + } + + /// Returns StdResult from validating a permit and then using its creator's address when + /// performing the specified query + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of contract's environment + /// * `permit` - the permit used to authentic the query + /// * `query` - the query to perform + pub fn permit_queries( + &self, + deps: Deps, + env: &Env, + permit: Permit, + query: QueryWithPermit, + ) -> StdResult { + let querier = deps.api.addr_canonicalize( + deps.api + .addr_validate(&validate( + deps, + PREFIX_REVOKED_PERMITS, + &permit, + env.contract.address.to_string(), + Some("secret"), + )?)? + .as_str(), + )?; + if !permit.check_permission(&secret_toolkit::permit::TokenPermissions::Owner) { + return Err(StdError::generic_err(format!( + "Owner permission is required for SNIP-721 queries, got permissions {:?}", + permit.params.permissions + ))); + } + let block = &env.block; + // permit validated, process query + match query { + QueryWithPermit::RoyaltyInfo { token_id } => { + self.query_royalty(deps, block, token_id.as_deref(), None, Some(querier)) + } + QueryWithPermit::PrivateMetadata { token_id } => { + self.query_private_meta(deps, block, &token_id, None, Some(querier)) + } + QueryWithPermit::NftDossier { + token_id, + include_expired, + } => { + self.query_nft_dossier(deps, block, token_id, None, include_expired, Some(querier)) + } + QueryWithPermit::BatchNftDossier { + token_ids, + include_expired, + } => self.query_batch_nft_dossier( + deps, + block, + token_ids, + None, + include_expired, + Some(querier), + ), + QueryWithPermit::OwnerOf { + token_id, + include_expired, + } => self.query_owner_of(deps, block, &token_id, None, include_expired, Some(querier)), + QueryWithPermit::AllNftInfo { + token_id, + include_expired, + } => self.query_all_nft_info( + deps, + block, + &token_id, + None, + include_expired, + Some(querier), + ), + QueryWithPermit::InventoryApprovals { include_expired } => { + self.query_inventory_approvals(deps, block, None, include_expired, Some(querier)) + } + QueryWithPermit::VerifyTransferApproval { token_ids } => { + self.query_verify_approval(deps, block, token_ids, None, Some(querier)) + } + QueryWithPermit::TransactionHistory { page, page_size } => { + self.query_transactions(deps, None, page, page_size, Some(querier)) + } + QueryWithPermit::NumTokens {} => self.query_num_tokens(deps, None, Some(querier)), + QueryWithPermit::AllTokens { start_after, limit } => { + self.query_all_tokens(deps, None, start_after.as_deref(), limit, Some(querier)) + } + QueryWithPermit::TokenApprovals { + token_id, + include_expired, + } => self.query_token_approvals( + deps, + block, + &token_id, + None, + include_expired, + Some(querier), + ), + QueryWithPermit::ApprovedForAll { include_expired } => { + self.query_approved_for_all(deps, block, None, None, include_expired, Some(querier)) + } + QueryWithPermit::Tokens { + owner, + start_after, + limit, + } => self.query_tokens( + deps, + block, + &owner, + None, + None, + start_after.as_deref(), + limit, + Some(querier), + ), + QueryWithPermit::NumTokensOfOwner { owner } => { + self.query_num_owner_tokens(deps, block, &owner, None, None, Some(querier)) + } + } + } + + /// Returns StdResult displaying the contract's creator + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + pub fn query_contract_creator(&self, deps: Deps) -> StdResult { + let creator_raw: CanonicalAddr = load(deps.storage, CREATOR_KEY)?; + to_binary(&QueryAnswer::ContractCreator { + creator: Some(deps.api.addr_humanize(&creator_raw)?), + }) + } + + /// Returns StdResult displaying the contract's name and symbol + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn query_contract_info(&self, storage: &dyn Storage) -> StdResult { + let config: Config = load(storage, CONFIG_KEY)?; + + to_binary(&ContractInfo { + name: config.name, + symbol: config.symbol, + }) + } + + /// Returns StdResult displaying either a token's royalty information or the contract's + /// default royalty information if no token_id is specified + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - optional token id whose RoyaltyInfo is being requested + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_royalty( + &self, + deps: Deps, + block: &BlockInfo, + token_id: Option<&str>, + viewer: Option, + from_permit: Option, + ) -> StdResult { + let viewer_raw = self.get_querier(deps, viewer, from_permit)?; + let (royalty, hide_addr) = if let Some(id) = token_id { + // if the token id was found + if let Ok((token, idx)) = self.get_token(deps.storage, id, None) { + let hide_addr = self + .check_perm_core( + deps, + block, + &token, + id, + viewer_raw.as_ref(), + token.owner.as_slice(), + PermissionType::Transfer.to_usize(), + &mut Vec::new(), + "", + ) + .is_err(); + // get the royalty information if present + let roy_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ROYALTY_INFO); + ( + may_load::(&roy_store, &idx.to_le_bytes())?, + hide_addr, + ) + // token id not found + } else { + let config: Config = load(deps.storage, CONFIG_KEY)?; + let minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + let is_minter = viewer_raw.map(|v| minters.contains(&v)).unwrap_or(false); + // if minter querying or the token supply is public, let them know the token does not exist + if config.token_supply_is_public || is_minter { + return Err(StdError::generic_err(format!("Token ID: {} not found", id))); + } + // token supply is private and querier is not a minter so just show the default without addresses + ( + may_load::(deps.storage, DEFAULT_ROYALTY_KEY)?, + true, + ) + } + // no id specified, so get the default + } else { + let minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + // only let minters view default royalty addresses + ( + may_load::(deps.storage, DEFAULT_ROYALTY_KEY)?, + viewer_raw.map(|v| !minters.contains(&v)).unwrap_or(true), + ) + }; + to_binary(&QueryAnswer::RoyaltyInfo { + royalty_info: royalty + .map(|s| s.to_human(deps.api, hide_addr)) + .transpose()?, + }) + } + + /// Returns StdResult displaying the contract's configuration + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn query_config(&self, storage: &dyn Storage) -> StdResult { + let config: Config = load(storage, CONFIG_KEY)?; + + to_binary(&QueryAnswer::ContractConfig { + token_supply_is_public: config.token_supply_is_public, + owner_is_public: config.owner_is_public, + sealed_metadata_is_enabled: config.sealed_metadata_is_enabled, + unwrapped_metadata_is_private: config.unwrap_to_private, + minter_may_update_metadata: config.minter_may_update_metadata, + owner_may_update_metadata: config.owner_may_update_metadata, + burn_is_enabled: config.burn_is_enabled, + implements_non_transferable_tokens: true, + implements_token_subtype: true, + }) + } + + /// Returns StdResult displaying the list of authorized minters + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + pub fn query_minters(&self, deps: Deps) -> StdResult { + let minters: Vec = may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + + to_binary(&Minters { + minters: minters + .iter() + .map(|m| deps.api.addr_humanize(m)) + .collect::>>()?, + }) + } + + /// Returns StdResult displaying the number of tokens the contract controls + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_num_tokens( + &self, + deps: Deps, + viewer: Option, + from_permit: Option, + ) -> StdResult { + // authenticate permission to view token supply + self.check_view_supply(deps, viewer, from_permit)?; + let config: Config = load(deps.storage, CONFIG_KEY)?; + to_binary(&NumTokens { + count: config.token_cnt, + }) + } + + /// Returns StdResult displaying the list of tokens that the contract controls + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `viewer` - optional address and key making an authenticated query request + /// * `start_after` - optionally only display token ids that come after this one + /// * `limit` - optional max number of tokens to display + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_all_tokens( + &self, + deps: Deps, + viewer: Option, + start_after: Option<&str>, + limit: Option, + from_permit: Option, + ) -> StdResult { + // authenticate permission to view token supply + self.check_view_supply(deps, viewer, from_permit)?; + let mut i = start_after.map_or_else( + || Ok(0), + |id| { + let map2idx = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MAP_TO_INDEX); + let idx: u32 = may_load(&map2idx, id.as_bytes())? + .ok_or_else(|| StdError::generic_err(format!("Token ID: {} not found", id)))?; + idx.checked_add(1).ok_or_else(|| { + StdError::generic_err("This token was the last one the contract could mint") + }) + }, + )?; + let cut_off = limit.unwrap_or(300); + let config: Config = load(deps.storage, CONFIG_KEY)?; + let mut tokens = Vec::new(); + let mut count = 0u32; + let map2id = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MAP_TO_ID); + while count < cut_off && i < config.mint_cnt { + if let Some(id) = may_load::(&map2id, &i.to_le_bytes())? { + tokens.push(id); + // will hit gas ceiling before the count overflows + count += 1; + } + // i can't overflow if it was less than a u32 + i += 1; + } + to_binary(&QueryAnswer::TokenList { tokens }) + } + + /// Returns StdResult displaying the owner of the input token if the requester is authorized + /// to view it and the transfer approvals on this token if the owner is querying + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - string slice of the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_owner_of( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let (may_owner, approvals, _idx) = self.process_cw721_owner_of( + deps, + block, + token_id, + viewer, + include_expired, + from_permit, + )?; + if let Some(owner) = may_owner { + return to_binary(&OwnerOf { owner, approvals }); + } + Err(StdError::generic_err(format!( + "You are not authorized to view the owner of token {}", + token_id + ))) + } + + /// Returns StdResult displaying the public metadata of a token + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + /// * `token_id` - string slice of the token id + pub fn query_nft_info(&self, storage: &dyn Storage, token_id: &str) -> StdResult { + let map2idx = ReadonlyPrefixedStorage::new(storage, PREFIX_MAP_TO_INDEX); + let may_idx: Option = may_load(&map2idx, token_id.as_bytes())?; + // if token id was found + if let Some(idx) = may_idx { + let meta_store = ReadonlyPrefixedStorage::new(storage, PREFIX_PUB_META); + let metadata_info = self + .token_extension_info + .get(storage, &token_id.to_string()); + let meta: Metadata = may_load(&meta_store, &idx.to_le_bytes())?.unwrap_or(Metadata { + token_uri: None, + extension: None, + }); + return to_binary(&NftInfo { + token_uri: meta.token_uri, + extension: meta.extension, + metadata_extension: metadata_info.unwrap(), + }); + } + let config: Config = load(storage, CONFIG_KEY)?; + // token id wasn't found + // if the token supply is public, let them know the token does not exist + if config.token_supply_is_public { + return Err(StdError::generic_err(format!( + "Token ID: {} not found", + token_id + ))); + } + // otherwise, just return empty metadata + to_binary(&QueryAnswer::NftInfo { + token_uri: None, + extension: None, + }) + } + + /// Returns StdResult displaying the private metadata of a token if permitted to + /// view it + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - string slice of the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_private_meta( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + viewer: Option, + from_permit: Option, + ) -> StdResult { + let prep_info = self.query_token_prep(deps, token_id, viewer, from_permit)?; + self.check_perm_core( + deps, + block, + &prep_info.token, + token_id, + prep_info.viewer_raw.as_ref(), + prep_info.token.owner.as_slice(), + PermissionType::ViewMetadata.to_usize(), + &mut Vec::new(), + &prep_info.err_msg, + )?; + // don't display if private metadata is sealed + if !prep_info.token.unwrapped { + return Err(StdError::generic_err( + "Sealed metadata must be unwrapped by calling Reveal before it can be viewed", + )); + } + let meta_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_PRIV_META); + let meta: Metadata = + may_load(&meta_store, &prep_info.idx.to_le_bytes())?.unwrap_or(Metadata { + token_uri: None, + extension: None, + }); + to_binary(&QueryAnswer::PrivateMetadata { + token_uri: meta.token_uri, + extension: meta.extension, + }) + } + + /// Returns StdResult displaying response of both the OwnerOf and NftInfo queries + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - string slice of the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_all_nft_info( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let (owner, approvals, idx) = self.process_cw721_owner_of( + deps, + block, + token_id, + viewer, + include_expired, + from_permit, + )?; + let meta_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_PUB_META); + let info: Option = may_load(&meta_store, &idx.to_le_bytes())?; + let access = Cw721OwnerOfResponse { owner, approvals }; + to_binary(&QueryAnswer::AllNftInfo { access, info }) + } + + /// Returns StdResult displaying all the token information the querier is permitted to + /// view. This may include the owner, the public metadata, the private metadata, royalty + /// information, mint run information, whether the token is unwrapped, whether the token is + /// transferable, and the token and inventory approvals + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_nft_dossier( + &self, + deps: Deps, + block: &BlockInfo, + token_id: String, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let dossier = self + .dossier_list( + deps, + block, + vec![token_id], + viewer, + include_expired, + from_permit, + )? + .pop() + .ok_or_else(|| { + StdError::generic_err("NftDossier can never return an empty dossier list") + })?; + + to_binary(&QueryAnswer::NftDossier { + owner: dossier.owner, + public_metadata: dossier.public_metadata, + private_metadata: dossier.private_metadata, + royalty_info: dossier.royalty_info, + mint_run_info: dossier.mint_run_info, + transferable: dossier.transferable, + unwrapped: dossier.unwrapped, + display_private_metadata_error: dossier.display_private_metadata_error, + owner_is_public: dossier.owner_is_public, + public_ownership_expiration: dossier.public_ownership_expiration, + private_metadata_is_public: dossier.private_metadata_is_public, + private_metadata_is_public_expiration: dossier.private_metadata_is_public_expiration, + token_approvals: dossier.token_approvals, + inventory_approvals: dossier.inventory_approvals, + }) + } + + /// Returns StdResult displaying all the token information the querier is permitted to + /// view of multiple tokens. This may include the owner, the public metadata, the private metadata, + /// royalty information, mint run information, whether the token is unwrapped, whether the token is + /// transferable, and the token and inventory approvals + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_ids` - list of token ids whose info should be retrieved + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_batch_nft_dossier( + &self, + deps: Deps, + block: &BlockInfo, + token_ids: Vec, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let nft_dossiers = + self.dossier_list(deps, block, token_ids, viewer, include_expired, from_permit)?; + + to_binary(&QueryAnswer::BatchNftDossier { nft_dossiers }) + } + + /// Returns StdResult displaying the approvals in place for a specified token + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - string slice of the token id + /// * `viewing_key` - the optional token owner's viewing key + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_token_approvals( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + viewing_key: Option<&str>, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let config: Config = load(deps.storage, CONFIG_KEY)?; + let custom_err = format!( + "You are not authorized to view approvals for token {}", + token_id + ); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + let (mut token, _idx) = self.get_token(deps.storage, token_id, opt_err)?; + // verify that the querier is the token owner + if let Some(pmt) = from_permit { + if pmt != token.owner { + return Err(StdError::generic_err(custom_err)); + } + } else { + let key = viewing_key.ok_or_else(|| { + StdError::generic_err("This is being called incorrectly if there is no viewing key") + })?; + let owner_addr = deps.api.addr_humanize(&token.owner)?; + ViewingKey::check(deps.storage, owner_addr.as_str(), key) + .map_err(|_| StdError::generic_err(VIEWING_KEY_ERR_MSG))?; + } + let owner_slice = token.owner.as_slice(); + let own_priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let global_pass: bool = + may_load(&own_priv_store, owner_slice)?.unwrap_or(config.owner_is_public); + let perm_type_info = PermissionTypeInfo { + view_owner_idx: PermissionType::ViewOwner.to_usize(), + view_meta_idx: PermissionType::ViewMetadata.to_usize(), + transfer_idx: PermissionType::Transfer.to_usize(), + num_types: PermissionType::Transfer.num_types(), + }; + let incl_exp = include_expired.unwrap_or(false); + let (token_approvals, token_owner_exp, token_meta_exp) = self.gen_snip721_approvals( + deps.api, + block, + &mut token.permissions, + incl_exp, + &perm_type_info, + )?; + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let mut all_perm: Vec = + json_may_load(&all_store, owner_slice)?.unwrap_or_default(); + let (_inventory_approv, all_owner_exp, all_meta_exp) = + self.gen_snip721_approvals(deps.api, block, &mut all_perm, incl_exp, &perm_type_info)?; + // determine if ownership is public + let (public_ownership_expiration, owner_is_public) = if global_pass { + (Some(Expiration::Never), true) + } else if token_owner_exp.is_some() { + (token_owner_exp, true) + } else { + (all_owner_exp, all_owner_exp.is_some()) + }; + // determine if private metadata is public + let (private_metadata_is_public_expiration, private_metadata_is_public) = + if token_meta_exp.is_some() { + (token_meta_exp, true) + } else { + (all_meta_exp, all_meta_exp.is_some()) + }; + to_binary(&QueryAnswer::TokenApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + }) + } + + /// Returns StdResult displaying the inventory-wide approvals for a specified address + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_inventory_approvals( + &self, + deps: Deps, + block: &BlockInfo, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + let owner_raw = self + .get_querier(deps, viewer, from_permit)? + .ok_or_else(|| { + StdError::generic_err( + "This is being called incorrectly if there is no querier address", + ) + })?; + let owner_slice = owner_raw.as_slice(); + let own_priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let config: Config = load(deps.storage, CONFIG_KEY)?; + let global_pass: bool = + may_load(&own_priv_store, owner_slice)?.unwrap_or(config.owner_is_public); + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let mut all_perm: Vec = + json_may_load(&all_store, owner_slice)?.unwrap_or_default(); + let perm_type_info = PermissionTypeInfo { + view_owner_idx: PermissionType::ViewOwner.to_usize(), + view_meta_idx: PermissionType::ViewMetadata.to_usize(), + transfer_idx: PermissionType::Transfer.to_usize(), + num_types: PermissionType::Transfer.num_types(), + }; + let ( + inventory_approvals, + mut public_ownership_expiration, + private_metadata_is_public_expiration, + ) = self.gen_snip721_approvals( + deps.api, + block, + &mut all_perm, + include_expired.unwrap_or(false), + &perm_type_info, + )?; + let owner_is_public = if global_pass { + public_ownership_expiration = Some(Expiration::Never); + true + } else { + public_ownership_expiration.is_some() + }; + let private_metadata_is_public = private_metadata_is_public_expiration.is_some(); + to_binary(&QueryAnswer::InventoryApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + inventory_approvals, + }) + } + + /// Returns StdResult displaying the list of all addresses that have approval to transfer + /// all of the owner's tokens. Only the owner's viewing key will be accepted for this query + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `owner` - an optional reference to the address whose transfer ALL list should be displayed + /// * `viewing_key` - optional owner's viewing key + /// * `include_expired` - optionally true if the Approval list should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_approved_for_all( + &self, + deps: Deps, + block: &BlockInfo, + owner: Option<&str>, + viewing_key: Option<&str>, + include_expired: Option, + from_permit: Option, + ) -> StdResult { + // get the address whose approvals are being queried + let owner_raw = if let Some(pmt) = from_permit { + pmt + } else { + let owner_addr = deps.api.addr_validate(owner.ok_or_else(|| { + StdError::generic_err( + "This is being called incorrectly if there is no owner address", + ) + })?)?; + let raw = deps.api.addr_canonicalize(owner_addr.as_str())?; + if let Some(key) = viewing_key { + ViewingKey::check(deps.storage, owner_addr.as_str(), key) + .map_err(|_| StdError::generic_err(VIEWING_KEY_ERR_MSG))?; + // didn't supply a viewing key so just return an empty list of approvals + } else { + return to_binary(&QueryAnswer::ApprovedForAll { + operators: Vec::new(), + }); + } + raw + }; + let mut operators: Vec = Vec::new(); + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = + json_may_load(&all_store, owner_raw.as_slice())?.unwrap_or_default(); + self.gen_cw721_approvals( + deps.api, + block, + &all_perm, + &mut operators, + PermissionType::Transfer.to_usize(), + include_expired.unwrap_or(false), + )?; + + to_binary(&QueryAnswer::ApprovedForAll { operators }) + } + + /// Returns StdResult displaying an optionally paginated list of all tokens belonging to + /// the owner address. It will only display the tokens that the querier has view_owner + /// approval + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `owner` - a reference to the address whose tokens should be displayed + /// * `viewer` - optional address of the querier if different from the owner + /// * `viewing_key` - optional viewing key String + /// * `start_after` - optionally only display token ids that come after this String in + /// lexicographical order + /// * `limit` - optional max number of tokens to display + /// * `from_permit` - address derived from an Owner permit, if applicable + #[allow(clippy::too_many_arguments)] + pub fn query_tokens( + &self, + deps: Deps, + block: &BlockInfo, + owner: &str, + viewer: Option<&str>, + viewing_key: Option<&str>, + start_after: Option<&str>, + limit: Option, + from_permit: Option, + ) -> StdResult { + let owner_addr = deps.api.addr_validate(owner)?; + let owner_raw = deps.api.addr_canonicalize(owner_addr.as_str())?; + let cut_off = limit.unwrap_or(30); + // determine the querier + let (is_owner, may_querier) = if let Some(pmt) = from_permit.as_ref() { + // permit tells you who is querying, so also check if he is the owner + (owner_raw == *pmt, from_permit) + // no permit, so check if a key was provided and who it matches + } else if let Some(key) = viewing_key { + // if there is a viewer + viewer + // validate the viewer address + .map(|v| deps.api.addr_validate(v)) + .transpose()? + // only keep the viewer address if the viewing key matches + .filter(|v| ViewingKey::check(deps.storage, v.as_str(), key).is_ok()) + .map_or_else( + // no viewer or key did not match + || { + // check if the key matches the owner, and error if it fails this last chance + ViewingKey::check(deps.storage, owner_addr.as_str(), key) + .map_err(|_| StdError::generic_err(VIEWING_KEY_ERR_MSG))?; + Ok::<(bool, Option), StdError>(( + true, + Some(owner_raw.clone()), + )) + }, + // we know the querier is the viewer, so check if someone put the same address for both + |v| { + let viewer_raw = deps.api.addr_canonicalize(v.as_str())?; + Ok((viewer_raw == owner_raw, Some(viewer_raw))) + }, + )? + // no permit, no viewing key, so querier is unknown + } else { + (false, None) + }; + // exit early if the limit is 0 + if cut_off == 0 { + return to_binary(&QueryAnswer::TokenList { tokens: Vec::new() }); + } + // get list of owner's tokens + let own_inv = Inventory::new(deps.storage, owner_raw)?; + let owner_slice = own_inv.owner.as_slice(); + + let querier = may_querier.as_ref(); + // if querier is different than the owner, check if ownership is public + let mut may_config: Option = None; + let mut known_pass = if !is_owner { + let config: Config = load(deps.storage, CONFIG_KEY)?; + let own_priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let pass: bool = + may_load(&own_priv_store, owner_slice)?.unwrap_or(config.owner_is_public); + may_config = Some(config); + pass + } else { + true + }; + let exp_idx = PermissionType::ViewOwner.to_usize(); + let info_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_INFOS); + let mut list_it: bool; + let mut oper_for: Vec = Vec::new(); + let mut tokens: Vec = Vec::new(); + let map2id = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MAP_TO_ID); + let mut inv_iter = if let Some(after) = start_after { + // load the config if we haven't already + let config = may_config.map_or_else(|| load::(deps.storage, CONFIG_KEY), Ok)?; + // if the querier is allowed to view all of the owner's tokens, let them know if the token + // does not belong to the owner + let inv_err = format!("Token ID: {} is not in the specified inventory", after); + // or tell any other viewer that they are not authorized + let unauth_err = format!( + "You are not authorized to perform this action on token {}", + after + ); + let public_err = format!("Token ID: {} not found", after); + // if token supply is public let them know if the token id does not exist + let not_found_err = if config.token_supply_is_public { + &public_err + } else if known_pass { + &inv_err + } else { + &unauth_err + }; + let map2idx = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MAP_TO_INDEX); + let idx: u32 = may_load(&map2idx, after.as_bytes())? + .ok_or_else(|| StdError::generic_err(not_found_err))?; + // make sure querier is allowed to know if the supplied token belongs to owner + if !known_pass { + let token: Token = json_may_load(&info_store, &idx.to_le_bytes())? + .ok_or_else(|| StdError::generic_err("Token info storage is corrupt"))?; + // if the specified token belongs to the specified owner, save if the querier is an operator + let mut may_oper_vec = if own_inv.owner == token.owner { + None + } else { + Some(Vec::new()) + }; + self.check_perm_core( + deps, + block, + &token, + after, + querier, + token.owner.as_slice(), + exp_idx, + may_oper_vec.as_mut().unwrap_or(&mut oper_for), + &unauth_err, + )?; + // if querier is found to have ALL permission for the specified owner, no need to check permission ever again + if !oper_for.is_empty() { + known_pass = true; + } + } + InventoryIter::start_after(deps.storage, &own_inv, idx, &inv_err)? + } else { + InventoryIter::new(&own_inv) + }; + let mut count = 0u32; + while let Some(idx) = inv_iter.next(deps.storage)? { + if let Some(id) = may_load::(&map2id, &idx.to_le_bytes())? { + list_it = known_pass; + // only check permissions if not public or owner + if !known_pass { + if let Some(token) = json_may_load::(&info_store, &idx.to_le_bytes())? { + list_it = self + .check_perm_core( + deps, + block, + &token, + &id, + querier, + owner_slice, + exp_idx, + &mut oper_for, + "", + ) + .is_ok(); + // if querier is found to have ALL permission, no need to check permission ever again + if !oper_for.is_empty() { + known_pass = true; + } + } + } + if list_it { + tokens.push(id); + // it'll hit the gas ceiling before overflowing the count + count += 1; + // exit if we hit the limit + if count >= cut_off { + break; + } + } + } + } + to_binary(&QueryAnswer::TokenList { tokens }) + } + + /// Returns StdResult displaying the number of tokens that the querier has permission to + /// view ownership and that belong to the specified address + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `owner` - a reference to the address whose tokens should be displayed + /// * `viewer` - optional address of the querier if different from the owner + /// * `viewing_key` - optional viewing key String + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_num_owner_tokens( + &self, + deps: Deps, + block: &BlockInfo, + owner: &str, + viewer: Option<&str>, + viewing_key: Option<&str>, + from_permit: Option, + ) -> StdResult { + let owner_addr = deps.api.addr_validate(owner)?; + let owner_raw = deps.api.addr_canonicalize(owner_addr.as_str())?; + // determine the querier + let (is_owner, may_querier) = if let Some(pmt) = from_permit.as_ref() { + // permit tells you who is querying, so also check if he is the owner + (owner_raw == *pmt, from_permit) + // no permit, so check if a key was provided and who it matches + } else if let Some(key) = viewing_key { + // if there is a viewer + viewer + // validate the viewer address + .map(|v| deps.api.addr_validate(v)) + .transpose()? + // only keep the viewer address if the viewing key matches + .filter(|v| ViewingKey::check(deps.storage, v.as_str(), key).is_ok()) + .map_or_else( + // no viewer or key did not match + || { + // check if the key matches the owner, and error if it fails this last chance + ViewingKey::check(deps.storage, owner_addr.as_str(), key) + .map_err(|_| StdError::generic_err(VIEWING_KEY_ERR_MSG))?; + Ok::<(bool, Option), StdError>(( + true, + Some(owner_raw.clone()), + )) + }, + // we know the querier is the viewer, so check if someone put the same address for both + |v| { + let viewer_raw = deps.api.addr_canonicalize(v.as_str())?; + Ok((viewer_raw == owner_raw, Some(viewer_raw))) + }, + )? + // no permit, no viewing key, so querier is unknown + } else { + (false, None) + }; + + // get list of owner's tokens + let own_inv = Inventory::new(deps.storage, owner_raw)?; + let owner_slice = own_inv.owner.as_slice(); + + // if querier is different than the owner, check if ownership is public + let mut known_pass = if !is_owner { + let config: Config = load(deps.storage, CONFIG_KEY)?; + let own_priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let pass: bool = + may_load(&own_priv_store, owner_slice)?.unwrap_or(config.owner_is_public); + pass + } else { + true + }; + let exp_idx = PermissionType::ViewOwner.to_usize(); + let global_raw = CanonicalAddr(Binary::from(b"public")); + let (sender, only_public) = if let Some(sdr) = may_querier.as_ref() { + (sdr, false) + } else { + (&global_raw, true) + }; + let mut found_one = only_public; + if !known_pass { + // check if the ownership has been made public or the sender has ALL permission. + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let may_list: Option> = json_may_load(&all_store, owner_slice)?; + if let Some(list) = may_list { + for perm in &list { + if perm.address == *sender || perm.address == global_raw { + if let Some(exp) = perm.expirations[exp_idx] { + if !exp.is_expired(block) { + known_pass = true; + break; + } + } + // we can quit if we found both the sender and the global (or if only searching for public) + if found_one { + break; + } else { + found_one = true; + } + } + } + } + } + // if it is either the owner, ownership is public, or the querier has inventory-wide view owner permission, + // let them see the full count + if known_pass { + return to_binary(&QueryAnswer::NumTokens { count: own_inv.cnt }); + } + + // get the list of tokens that might have viewable ownership for this querier + let mut token_idxs: HashSet = HashSet::new(); + found_one = only_public; + let auth_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = may_load(&auth_store, owner_slice)?.unwrap_or_default(); + for auth in auth_list.iter() { + if auth.address == *sender || auth.address == global_raw { + token_idxs.extend(auth.tokens[exp_idx].iter()); + if found_one { + break; + } else { + found_one = true; + } + } + } + // check if the the token permissions have expired, and if not include it in the count + let info_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_INFOS); + let mut count = 0u32; + for idx in token_idxs.into_iter() { + if let Some(token) = json_may_load::(&info_store, &idx.to_le_bytes())? { + found_one = only_public; + for perm in token.permissions.iter() { + if perm.address == *sender || perm.address == global_raw { + if let Some(exp) = perm.expirations[exp_idx] { + if !exp.is_expired(block) { + count += 1; + break; + } + } + // we can quit if we found both the sender and the global (or if only searching for public) + if found_one { + break; + } else { + found_one = true; + } + } + } + } + } + + to_binary(&QueryAnswer::NumTokens { count }) + } + + /// Returns StdResult displaying true if the token has been unwrapped. If sealed metadata + /// is not enabled, all tokens are considered unwrapped + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn query_is_unwrapped(&self, storage: &dyn Storage, token_id: &str) -> StdResult { + let config: Config = load(storage, CONFIG_KEY)?; + let get_token_res = self.get_token(storage, token_id, None); + match get_token_res { + Err(err) => match err { + // if the token id is not found, but token supply is private, just say + // the token's wrapped state is the same as a newly minted token + StdError::GenericErr { msg, .. } + if !config.token_supply_is_public && msg.contains("Token ID") => + { + to_binary(&QueryAnswer::IsUnwrapped { + token_is_unwrapped: !config.sealed_metadata_is_enabled, + }) + } + _ => Err(err), + }, + Ok((token, _idx)) => to_binary(&QueryAnswer::IsUnwrapped { + token_is_unwrapped: token.unwrapped, + }), + } + } + + /// Returns StdResult displaying true if the token is transferable + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn query_is_transferable( + &self, + storage: &dyn Storage, + token_id: &str, + ) -> StdResult { + let config: Config = load(storage, CONFIG_KEY)?; + let get_token_res = self.get_token(storage, token_id, None); + match get_token_res { + Err(err) => match err { + // if the token id is not found, but token supply is private, just say + // the token is transferable + StdError::GenericErr { msg, .. } + if !config.token_supply_is_public && msg.contains("Token ID") => + { + to_binary(&QueryAnswer::IsTransferable { + token_is_transferable: true, + }) + } + _ => Err(err), + }, + Ok((token, _idx)) => to_binary(&QueryAnswer::IsTransferable { + token_is_transferable: token.transferable, + }), + } + } + + /// Returns StdResult displaying an optionally paginated list of all transactions + /// involving a specified address, displayed in reverse chronological order + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `viewer` - optional address and key making an authenticated query request + /// * `page` - an optional page number. If given, the most recent `page` times `page_size` + /// transactions will be skipped + /// * `page_size` - optional max number of transactions to display + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn query_transactions( + &self, + deps: Deps, + viewer: Option, + page: Option, + page_size: Option, + from_permit: Option, + ) -> StdResult { + let address_raw = self + .get_querier(deps, viewer, from_permit)? + .ok_or_else(|| { + StdError::generic_err( + "This is being called incorrectly if there is no querier address", + ) + })?; + let (txs, total) = get_txs( + deps.api, + deps.storage, + &address_raw, + page.unwrap_or(0), + page_size.unwrap_or(30), + )?; + to_binary(&QueryAnswer::TransactionHistory { total, txs }) + } + + /// Returns StdResult after verifying that the specified address has transfer approval + /// for all the listed tokens. A token will count as unapproved if it is non-transferable + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_ids` - a list of token ids to check if the address has transfer approval + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + #[allow(unknown_lints)] + pub fn query_verify_approval( + &self, + deps: Deps, + block: &BlockInfo, + token_ids: Vec, + viewer: Option, + from_permit: Option, + ) -> StdResult { + let address_raw = self + .get_querier(deps, viewer, from_permit)? + .ok_or_else(|| { + StdError::generic_err( + "This is being called incorrectly if there is no querier address", + ) + })?; + let config: Config = load(deps.storage, CONFIG_KEY)?; + let mut oper_for: Vec = Vec::new(); + for id in token_ids.into_iter() { + // cargo fmt creates the and_then block, but clippy doesn't like it + #[allow(clippy::blocks_in_conditions)] + if self + .get_token_if_permitted( + deps, + block, + &id, + Some(&address_raw), + PermissionType::Transfer, + &mut oper_for, + &config, + // the and_then forces an error if the token is not transferable + ) + .and_then(|(t, _)| { + if t.transferable { + Ok(()) + } else { + // the msg is never seen + Err(StdError::generic_err("")) + } + }) + .is_err() + { + return to_binary(&QueryAnswer::VerifyTransferApproval { + approved_for_all: false, + first_unapproved_token: Some(id), + }); + } + } + to_binary(&QueryAnswer::VerifyTransferApproval { + approved_for_all: true, + first_unapproved_token: None, + }) + } + + /// Returns StdResult displaying the registered code hash of the specified contract if + /// it has registered and whether the contract implements BatchReceiveNft + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `contract` - a reference to the contract's address whose code hash is being requested + pub fn query_code_hash(&self, deps: Deps, contract: &str) -> StdResult { + let contract_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(contract)?.as_str())?; + let store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_RECEIVERS); + let may_reg_rec: Option = may_load(&store, contract_raw.as_slice())?; + if let Some(reg_rec) = may_reg_rec { + return to_binary(&QueryAnswer::RegisteredCodeHash { + code_hash: Some(reg_rec.code_hash), + also_implements_batch_receive_nft: reg_rec.impl_batch, + }); + } + to_binary(&QueryAnswer::RegisteredCodeHash { + code_hash: None, + also_implements_batch_receive_nft: false, + }) + } + + /// Returns StdResult after performing common preparations for authenticated + /// token queries + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `token_id` - string slice of the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + fn query_token_prep( + &self, + deps: Deps, + token_id: &str, + viewer: Option, + from_permit: Option, + ) -> StdResult { + let viewer_raw = self.get_querier(deps, viewer, from_permit)?; + let config: Config = load(deps.storage, CONFIG_KEY)?; + let err_msg = format!( + "You are not authorized to perform this action on token {}", + token_id + ); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*err_msg) + }; + let (token, idx) = self.get_token(deps.storage, token_id, opt_err)?; + Ok(TokenQueryInfo { + viewer_raw, + err_msg, + token, + idx, + owner_is_public: config.owner_is_public, + }) + } + + /// Returns StdResult<(Option, Vec, u32)> which is the owner, list of transfer + /// approvals, and token index of the request token + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_id` - string slice of the token id + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + fn process_cw721_owner_of( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult<(Option, Vec, u32)> { + let prep_info = self.query_token_prep(deps, token_id, viewer, from_permit)?; + let opt_viewer = prep_info.viewer_raw.as_ref(); + if self + .check_permission( + deps, + block, + &prep_info.token, + token_id, + opt_viewer, + PermissionType::ViewOwner, + &mut Vec::new(), + &prep_info.err_msg, + prep_info.owner_is_public, + ) + .is_ok() + { + let (owner, mut approvals, mut operators) = self.get_owner_of_resp( + deps, + block, + &prep_info.token, + opt_viewer, + include_expired.unwrap_or(false), + )?; + approvals.append(&mut operators); + return Ok((Some(owner), approvals, prep_info.idx)); + } + Ok((None, Vec::new(), prep_info.idx)) + } + + /// Returns StdResult<(Addr, Vec, Vec)> + /// which is the owner, token transfer Approval list, and Approval list of everyone + /// that can transfer all of the token owner's tokens + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `token` - a reference to the token whose owner info is being requested + /// * `viewer` - optional reference to the address requesting to view the owner + /// * `include_expired` - true if the Approval lists should include expired Approvals + fn get_owner_of_resp( + &self, + deps: Deps, + block: &BlockInfo, + token: &Token, + viewer: Option<&CanonicalAddr>, + include_expired: bool, + ) -> StdResult<(Addr, Vec, Vec)> { + let owner = deps.api.addr_humanize(&token.owner)?; + let mut spenders: Vec = Vec::new(); + let mut operators: Vec = Vec::new(); + if let Some(vwr) = viewer { + if token.owner == *vwr { + let transfer_idx = PermissionType::Transfer.to_usize(); + self.gen_cw721_approvals( + deps.api, + block, + &token.permissions, + &mut spenders, + transfer_idx, + include_expired, + )?; + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = + json_may_load(&all_store, token.owner.as_slice())?.unwrap_or_default(); + self.gen_cw721_approvals( + deps.api, + block, + &all_perm, + &mut operators, + transfer_idx, + include_expired, + )?; + } + } + Ok((owner, spenders, operators)) + } + + /// Returns StdResult<()> resulting from generating the list of Approvals + /// + /// # Arguments + /// + /// * `api` - reference to the Api used to convert canonical and human addresses + /// * `block` - a reference to the current BlockInfo + /// * `perm_list` - slice of Permissions to search through looking for transfer approvals + /// * `approvals` - a mutable reference to the list of approvals that should be appended + /// with any found in the permission list + /// * `transfer_idx` - index into the Permission expirations that represents transfers + /// * `include_expired` - true if the Approval list should include expired Approvals + fn gen_cw721_approvals( + &self, + api: &dyn Api, + block: &BlockInfo, + perm_list: &[Permission], + approvals: &mut Vec, + transfer_idx: usize, + include_expired: bool, + ) -> StdResult<()> { + let global_raw = CanonicalAddr(Binary::from(b"public")); + for perm in perm_list { + if let Some(exp) = perm.expirations[transfer_idx] { + if (include_expired || !exp.is_expired(block)) && perm.address != global_raw { + approvals.push(Cw721Approval { + spender: api.addr_humanize(&perm.address)?, + expires: exp, + }); + } + } + } + Ok(()) + } + + /// Returns StdResult<(Vec, Option, Option)> + /// which is the list of approvals, an optional Expiration if ownership is public, and + /// an optional Expiration if the private metadata is public + /// + /// # Arguments + /// + /// * `api` - reference to the Api used to convert canonical and human addresses + /// * `block` - a reference to the current BlockInfo + /// * `perm_list` - a mutable reference to the list of Permission + /// * `include_expired` - true if the Approval list should include expired Approvals + /// * `perm_type_info` - a reference to PermissionTypeInfo + fn gen_snip721_approvals( + &self, + api: &dyn Api, + block: &BlockInfo, + perm_list: &mut Vec, + include_expired: bool, + perm_type_info: &PermissionTypeInfo, + ) -> StdResult<(Vec, Option, Option)> { + let global_raw = CanonicalAddr(Binary::from(b"public")); + let mut approvals: Vec = Vec::new(); + let mut owner_public: Option = None; + let mut meta_public: Option = None; + for perm in perm_list { + // set global permissions if present + if perm.address == global_raw { + if let Some(exp) = perm.expirations[perm_type_info.view_owner_idx] { + if !exp.is_expired(block) { + owner_public = Some(exp); + } + } + if let Some(exp) = perm.expirations[perm_type_info.view_meta_idx] { + if !exp.is_expired(block) { + meta_public = Some(exp); + } + } + // otherwise create the approval summary + } else { + let mut has_some = false; + for i in 0..perm_type_info.num_types { + perm.expirations[i] = + perm.expirations[i].filter(|e| include_expired || !e.is_expired(block)); + if !has_some && perm.expirations[i].is_some() { + has_some = true; + } + } + if has_some { + approvals.push(Snip721Approval { + address: api.addr_humanize(&perm.address)?, + view_owner_expiration: perm.expirations[perm_type_info.view_owner_idx] + .take(), + view_private_metadata_expiration: perm.expirations + [perm_type_info.view_meta_idx] + .take(), + transfer_expiration: perm.expirations[perm_type_info.transfer_idx].take(), + }); + } + } + } + Ok((approvals, owner_public, meta_public)) + } + + /// Returns StdResult<()> + /// + /// returns Ok if authorized to view token supply, Err otherwise + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - address derived from an Owner permit, if applicable + fn check_view_supply( + &self, + deps: Deps, + viewer: Option, + from_permit: Option, + ) -> StdResult<()> { + let config: Config = load(deps.storage, CONFIG_KEY)?; + let mut is_auth = config.token_supply_is_public; + if !is_auth { + let querier = self.get_querier(deps, viewer, from_permit)?; + if let Some(viewer_raw) = querier { + let minters: Vec = + may_load(deps.storage, MINTERS_KEY)?.unwrap_or_default(); + is_auth = minters.contains(&viewer_raw); + } + if !is_auth { + return Err(StdError::generic_err( + "The token supply of this contract is private", + )); + } + } + Ok(()) + } + + /// Returns StdResult<()> + /// + /// returns Ok if the address has permission or an error if not + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `token` - a reference to the token + /// * `token_id` - token ID String slice + /// * `opt_sender` - a optional reference to the address trying to get access to the token + /// * `perm_type` - PermissionType we are checking + /// * `oper_for` - a mutable reference to a list of owners that gave the sender "all" permission + /// * `custom_err` - string slice of the error msg to return if not permitted + /// * `owner_is_public` - true if token ownership is public for this contract + #[allow(clippy::too_many_arguments)] + pub fn check_permission( + &self, + deps: Deps, + block: &BlockInfo, + token: &Token, + token_id: &str, + opt_sender: Option<&CanonicalAddr>, + perm_type: PermissionType, + oper_for: &mut Vec, + custom_err: &str, + owner_is_public: bool, + ) -> StdResult<()> { + let exp_idx = perm_type.to_usize(); + let owner_slice = token.owner.as_slice(); + // check if owner is public/private. use owner's setting if present, contract default + // if not + if let PermissionType::ViewOwner = perm_type { + let priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let pass: bool = may_load(&priv_store, owner_slice)?.unwrap_or(owner_is_public); + if pass { + return Ok(()); + } + } + self.check_perm_core( + deps, + block, + token, + token_id, + opt_sender, + owner_slice, + exp_idx, + oper_for, + custom_err, + ) + } + + /// Returns StdResult<()> + /// + /// returns Ok if the address has permission or an error if not + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `token` - a reference to the token + /// * `token_id` - token ID String slice + /// * `opt_sender` - a optional reference to the address trying to get access to the token + /// * `owner_slice` - the owner of the token represented as a byte slice + /// * `exp_idx` - permission type we are checking represented as usize + /// * `oper_for` - a mutable reference to a list of owners that gave the sender "all" permission + /// * `custom_err` - string slice of the error msg to return if not permitted + #[allow(clippy::too_many_arguments)] + fn check_perm_core( + &self, + deps: Deps, + block: &BlockInfo, + token: &Token, + token_id: &str, + opt_sender: Option<&CanonicalAddr>, + owner_slice: &[u8], + exp_idx: usize, + oper_for: &mut Vec, + custom_err: &str, + ) -> StdResult<()> { + // if did not already pass with "all" permission for this owner + if !oper_for.contains(&token.owner) { + let mut err_msg = custom_err; + let mut expired_msg = String::new(); + let global_raw = CanonicalAddr(Binary::from(b"public")); + let (sender, only_public) = if let Some(sdr) = opt_sender { + (sdr, false) + } else { + (&global_raw, true) + }; + // if this is the owner, all is good + if token.owner == *sender { + return Ok(()); + } + // check if the token is public or the sender has token permission. + // Can't use find because even if the global or sender permission expired, you + // still want to see if the other is still valid, but if we are only checking for public + // we can quit after one failure + let mut one_expired = only_public; + // if we are only checking for public permission, we can quit after one failure + let mut found_one = only_public; + for perm in &token.permissions { + if perm.address == *sender || perm.address == global_raw { + if let Some(exp) = perm.expirations[exp_idx] { + if !exp.is_expired(block) { + return Ok(()); + // if the permission is expired + } else { + // if this is the sender let them know the permission expired + if perm.address != global_raw { + expired_msg + .push_str(&format!("Access to token {} has expired", token_id)); + err_msg = &expired_msg; + } + // if both were expired (or only checking for global), there can't be any ALL permissions + // so just exit early + if one_expired { + return Err(StdError::generic_err(err_msg)); + } else { + one_expired = true; + } + } + } + // we can quit if we found both the sender and the global (or only checking global) + if found_one { + break; + } else { + found_one = true; + } + } + } + // check if the entire permission type is public or the sender has ALL permission. + // Can't use find because even if the global or sender permission expired, you + // still want to see if the other is still valid, but if we are only checking for public + // we can quit after one failure + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + let may_list: Option> = json_may_load(&all_store, owner_slice)?; + found_one = only_public; + if let Some(list) = may_list { + for perm in &list { + if perm.address == *sender || perm.address == global_raw { + if let Some(exp) = perm.expirations[exp_idx] { + if !exp.is_expired(block) { + oper_for.push(token.owner.clone()); + return Ok(()); + // if the permission expired and this is the sender let them know the + // permission expired + } else if perm.address != global_raw { + expired_msg.push_str(&format!( + "Access to all tokens of {} has expired", + &deps.api.addr_humanize(&token.owner)? + )); + err_msg = &expired_msg; + } + } + // we can quit if we found both the sender and the global (or only checking global) + if found_one { + return Err(StdError::generic_err(err_msg)); + } else { + found_one = true; + } + } + } + } + return Err(StdError::generic_err(err_msg)); + } + Ok(()) + } + + /// Returns StdResult<(Token, u32)> + /// + /// returns the token information if the sender has authorization + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `token_id` - token ID String slice + /// * `sender` - a optional reference to the address trying to get access to the token + /// * `perm_type` - PermissionType we are checking + /// * `oper_for` - a mutable reference to a list of owners that gave the sender "all" permission + /// * `config` - a reference to the Config + #[allow(clippy::too_many_arguments)] + fn get_token_if_permitted( + &self, + deps: Deps, + block: &BlockInfo, + token_id: &str, + sender: Option<&CanonicalAddr>, + perm_type: PermissionType, + oper_for: &mut Vec, + config: &Config, + ) -> StdResult<(Token, u32)> { + let custom_err = format!( + "You are not authorized to perform this action on token {}", + token_id + ); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*custom_err) + }; + let (token, idx) = self.get_token(deps.storage, token_id, opt_err)?; + self.check_permission( + deps, + block, + &token, + token_id, + sender, + perm_type, + oper_for, + &custom_err, + config.owner_is_public, + )?; + Ok((token, idx)) + } + + /// Returns StdResult<(Token, u32)> + /// + /// returns the specified token and its identifier index + /// + /// # Arguments + /// + /// * `storage` - a reference to contract's storage + /// * `token_id` - token id string slice + /// * `custom_err` - optional custom error message to use if don't want to reveal that a token + /// does not exist + fn get_token( + &self, + storage: &dyn Storage, + token_id: &str, + custom_err: Option<&str>, + ) -> StdResult<(Token, u32)> { + let default_err: String; + let not_found = if let Some(err) = custom_err { + err + } else { + default_err = format!("Token ID: {} not found", token_id); + &*default_err + }; + let map2idx = ReadonlyPrefixedStorage::new(storage, PREFIX_MAP_TO_INDEX); + let idx: u32 = may_load(&map2idx, token_id.as_bytes())? + .ok_or_else(|| StdError::generic_err(not_found))?; + let info_store = ReadonlyPrefixedStorage::new(storage, PREFIX_INFOS); + let token: Token = json_may_load(&info_store, &idx.to_le_bytes())?.ok_or_else(|| { + StdError::generic_err(format!("Unable to find token info for {}", token_id)) + })?; + Ok((token, idx)) + } + + /// Returns StdResult<()> that will error if the priority level of the action is not + /// equal to or greater than the current contract status level + /// + /// # Arguments + /// + /// * `contract_status` - u8 representation of the current contract status + /// * `priority` - u8 representing the highest status level this action may execute at + fn check_status(&self, contract_status: u8, priority: u8) -> StdResult<()> { + if priority < contract_status { + return Err(StdError::generic_err( + "The contract admin has temporarily disabled this action", + )); + } + Ok(()) + } + + /// Returns StdResult<()> + /// + /// sets new metadata + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + /// * `token` - a reference to the token whose metadata should be updated + /// * `idx` - the token identifier index + /// * `prefix` - storage prefix for the type of metadata being updated + /// * `metadata` - a reference to the new metadata + #[allow(clippy::too_many_arguments)] + fn set_metadata_impl( + &self, + storage: &mut dyn Storage, + token: &Token, + idx: u32, + prefix: &[u8], + metadata: &Metadata, + ) -> StdResult<()> { + // do not allow the altering of sealed metadata + if !token.unwrapped && prefix == PREFIX_PRIV_META { + return Err(StdError::generic_err( + "The private metadata of a sealed token can not be modified", + )); + } + self.enforce_metadata_field_exclusion(metadata)?; + let mut meta_store = PrefixedStorage::new(storage, prefix); + save(&mut meta_store, &idx.to_le_bytes(), metadata)?; + Ok(()) + } + + /// Returns StdResult<()> + /// + /// sets specified permissions for an address + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + /// * `env` - a reference to the Env of the contract's environment + /// * `address` - a reference to the address being granted/revoked permission + /// * `owner` - a reference to the permission owner's address + /// * `proc_info` - a mutable reference to the ProcessAccInfo + /// * `all_perm_in` - when from an operator, the all_perms have already been read + fn process_accesses( + &self, + storage: &mut dyn Storage, + env: &Env, + address: &CanonicalAddr, + owner: &CanonicalAddr, + proc_info: &mut ProcessAccInfo, + all_perm_in: Option>, + ) -> StdResult<()> { + let owner_slice = owner.as_slice(); + let expiration = proc_info.expires.unwrap_or_default(); + let expirations = vec![expiration; 3]; + let mut alt_all_perm = AlterPermTable::default(); + let mut alt_tok_perm = AlterPermTable::default(); + let mut alt_load_tok_perm = AlterPermTable::default(); + let mut alt_auth_list = AlterAuthTable::default(); + let mut add_load_list = Vec::new(); + let mut load_all = false; + let mut load_all_exp = vec![Expiration::AtHeight(0); 3]; + let mut all_perm = if proc_info.from_oper { + all_perm_in.ok_or_else(|| StdError::generic_err("Unable to get operator list"))? + } else { + Vec::new() + }; + let mut oper_pos = 0usize; + let mut found_perm = false; + let mut tried_oper = false; + let num_perm_types = PermissionType::ViewOwner.num_types(); + + // do every permission type + for i in 0..num_perm_types { + if let Some(acc) = &proc_info.accesses[i] { + match acc { + AccessLevel::ApproveToken | AccessLevel::RevokeToken => { + if !proc_info.token_given { + return Err(StdError::generic_err( + "Attempted to grant/revoke permission for a token, but did not specify a token ID", + )); + } + let is_approve = matches!(acc, AccessLevel::ApproveToken); + // load the "all" permissions if we haven't already and see if the address is there + if !tried_oper { + if !proc_info.from_oper { + let all_store = + ReadonlyPrefixedStorage::new(storage, PREFIX_ALL_PERMISSIONS); + all_perm = + json_may_load(&all_store, owner_slice)?.unwrap_or_default(); + } + if let Some(pos) = all_perm.iter().position(|p| p.address == *address) { + found_perm = true; + oper_pos = pos; + } + tried_oper = true; + } + // if this address has "all" permission + if found_perm { + if let Some(op) = all_perm.get(oper_pos) { + if let Some(exp) = op.expirations[i] { + if !exp.is_expired(&env.block) { + // don't allow one operator to change to another + // operator's permissions + if proc_info.from_oper { + // if adding, don't do anything + if is_approve { + return Ok(()); + // if revoking, throw error + } else { + return Err(StdError::generic_err( + "Can not revoke transfer permission from an existing operator", + )); + } + } + // if you are granting token approval to an existing + // operator, but not changing the expiration, nothing + // needs to be done + if is_approve && expirations[i] == exp { + continue; + } + // need to put all the other tokens in the AuthList + alt_auth_list.full[i] = true; + // going to load all the other tokens + load_all = true; + // and use the "all" expiration as the token permission expirations + load_all_exp[i] = exp; + // add this address to all the other token permissions + alt_load_tok_perm.add[i] = true; + alt_load_tok_perm.has_update = true; + } + // remove "all" permission + alt_all_perm.remove[i] = true; + alt_all_perm.has_update = true; + } + } + } + if is_approve { + // add permission for this token + alt_tok_perm.add[i] = true; + // add this token to the authList + alt_auth_list.add[i] = true; + } else { + // revoke permission for this token + alt_tok_perm.remove[i] = true; + // remove this token from the AuthList + alt_auth_list.remove[i] = true; + } + alt_tok_perm.has_update = true; + alt_auth_list.has_update = true; + } + AccessLevel::All | AccessLevel::None => { + if let AccessLevel::All = acc { + // add "all" permission + alt_all_perm.add[i] = true; + } else { + // remove "all" permission + alt_all_perm.remove[i] = true; + } + alt_all_perm.has_update = true; + // clear the AuthList + alt_auth_list.clear[i] = true; + alt_auth_list.has_update = true; + // if a token was specified + if proc_info.token_given { + // also remove that token permission + alt_tok_perm.remove[i] = true; + alt_tok_perm.has_update = true; + } + // remove all other token permissions + alt_load_tok_perm.remove[i] = true; + alt_load_tok_perm.has_update = true; + // if not already going to load every owned token + if !load_all { + // load the AuthList tokens for this permission type + add_load_list.push(i); + } + } + } + } + } + // update "all" permissions + if alt_all_perm.has_update { + // load "all" permissions if we haven't already + if !tried_oper { + let all_store = ReadonlyPrefixedStorage::new(storage, PREFIX_ALL_PERMISSIONS); + all_perm = json_may_load(&all_store, owner_slice)?.unwrap_or_default(); + } + // if there was an update to the "all" permissions + if self.alter_perm_list( + &mut all_perm, + &alt_all_perm, + address, + &expirations, + num_perm_types, + ) { + let mut all_store = PrefixedStorage::new(storage, PREFIX_ALL_PERMISSIONS); + // if deleted last permitted address + if all_perm.is_empty() { + remove(&mut all_store, owner_slice); + } else { + json_save(&mut all_store, owner_slice, &all_perm)?; + } + } + } + // update input token permissions. + // Shouldn't need to check if token was given because if it wasn't we would have thrown an + // error before setting the has_update flag, but let's include the check anyway + if alt_tok_perm.has_update && proc_info.token_given { + // if there was an update to the token permissions + if self.alter_perm_list( + &mut proc_info.token.permissions, + &alt_tok_perm, + address, + &expirations, + num_perm_types, + ) { + let mut info_store = PrefixedStorage::new(storage, PREFIX_INFOS); + json_save( + &mut info_store, + &proc_info.idx.to_le_bytes(), + &proc_info.token, + )?; + } + } + // update the owner's AuthLists + if alt_auth_list.has_update { + // get the AuthLists for this address + let auth_store = ReadonlyPrefixedStorage::new(storage, PREFIX_AUTHLIST); + let mut auth_list: Vec = + may_load(&auth_store, owner_slice)?.unwrap_or_default(); + let mut new_auth = AuthList { + address: address.clone(), + tokens: [Vec::new(), Vec::new(), Vec::new()], + }; + let (auth, found, pos) = + if let Some(pos) = auth_list.iter().position(|a| a.address == *address) { + if let Some(a) = auth_list.get_mut(pos) { + (a, true, pos) + // shouldn't ever find it but not successfully get it, so this should never happen + } else { + (&mut new_auth, false, 0usize) + } + // didn't find the address in the permission list + } else { + (&mut new_auth, false, 0usize) + }; + let load_list: HashSet = if alt_load_tok_perm.has_update { + // if we need to load other tokens create the load list + // if we are loading all the owner's other tokens + let list = if load_all { + let inv = Inventory::new(storage, owner.clone())?; + let mut set = inv.to_set(storage)?; + // above, we already processed the input token, so remove it from the load list + set.remove(&proc_info.idx); + set + // just loading the tokens in the appropriate AuthList + } else { + let mut set: HashSet = HashSet::new(); + for l in add_load_list { + set.extend(auth.tokens[l].iter()); + } + // don't load the input token if given + if proc_info.token_given { + set.remove(&proc_info.idx); + } + set + }; + let mut info_store = PrefixedStorage::new(storage, PREFIX_INFOS); + for t_i in &list { + let tok_key = t_i.to_le_bytes(); + let may_tok: Option = json_may_load(&info_store, &tok_key)?; + if let Some(mut load_tok) = may_tok { + // shouldn't ever fail this ownership check, but let's be safe + if load_tok.owner == *owner + && self.alter_perm_list( + &mut load_tok.permissions, + &alt_load_tok_perm, + address, + &load_all_exp, + num_perm_types, + ) + { + json_save(&mut info_store, &tok_key, &load_tok)?; + } + } + } + list + } else { + HashSet::new() + }; + let mut updated = false; + // do for each PermissionType + for i in 0..num_perm_types { + // if revoked all individual token permissions + if alt_auth_list.clear[i] { + if !auth.tokens[i].is_empty() { + auth.tokens[i].clear(); + updated = true; + } + // else if gave permission to all individual tokens (except the input token) + } else if alt_auth_list.full[i] { + auth.tokens[i] = load_list.iter().copied().collect(); + // if this was an ApproveToken done to an address with ALL permission + // also add the specified token + if alt_auth_list.add[i] { + auth.tokens[i].push(proc_info.idx); + } + updated = true; + // else if just adding the input token (shouldn't need the token_given check) + } else if alt_auth_list.add[i] && proc_info.token_given { + if !auth.tokens[i].contains(&proc_info.idx) { + auth.tokens[i].push(proc_info.idx); + updated = true; + } + // else if just revoking perm on the input token (don't need the token_given check) + } else if alt_auth_list.remove[i] && proc_info.token_given { + if let Some(tok_pos) = auth.tokens[i].iter().position(|&t| t == proc_info.idx) { + auth.tokens[i].swap_remove(tok_pos); + updated = true; + } + } + } + // if a change was made + if updated { + let mut auth_store = PrefixedStorage::new(storage, PREFIX_AUTHLIST); + let mut save_it = true; + // if the address has no authorized tokens + if auth.tokens.iter().all(|t| t.is_empty()) { + // and it was a pre-existing AuthList + if found { + // if it was the only authorized address, + // remove the storage entry + if auth_list.len() == 1 { + remove(&mut auth_store, owner_slice); + save_it = false; + } else { + auth_list.swap_remove(pos); + } + // address had no previous authorization so no need to add it + } else { + save_it = false; + } + // AuthList has data, so save it + } else { + // if it is a new address, add it to the list + if !found { + auth_list.push(new_auth); + } + } + if save_it { + save(&mut auth_store, owner_slice, &auth_list)?; + } + } + } + Ok(()) + } + + /// Returns bool + /// + /// adds or removes permissions for an address based on the alterations table + /// + /// # Arguments + /// + /// * `perms` - a mutable reference to the list of permissions + /// * `alter_table` - a reference to the AlterPermTable to drive the Permission changes + /// * `address` - a reference to the address being added/revoked permission + /// * `expiration` - slice of Expirations for each PermissionType + /// * `num_perm_types` - the number of permission types + fn alter_perm_list( + &self, + perms: &mut Vec, + alter_table: &AlterPermTable, + address: &CanonicalAddr, + expiration: &[Expiration], + num_perm_types: usize, + ) -> bool { + let mut updated = false; + let mut new_perm = Permission { + address: address.clone(), + expirations: [None; 3], + }; + let (perm, found, pos) = if let Some(pos) = perms.iter().position(|p| p.address == *address) + { + if let Some(p) = perms.get_mut(pos) { + (p, true, pos) + // shouldn't ever find it but not successfully get it, so this should never happen + } else { + (&mut new_perm, false, 0usize) + } + // didn't find the address in the permission list + } else { + (&mut new_perm, false, 0usize) + }; + // do for each PermissionType + #[allow(clippy::needless_range_loop)] + for i in 0..num_perm_types { + // if supposed to add permission + if alter_table.add[i] { + // if it already has permission for this type + if let Some(old_exp) = perm.expirations[i] { + // if the new expiration is different + if old_exp != expiration[i] { + perm.expirations[i] = Some(expiration[i]); + updated = true; + } + // new permission + } else { + perm.expirations[i] = Some(expiration[i]); + updated = true; + } + // otherwise if we are supposed to remove permission + } else if alter_table.remove[i] { + // if it has permission for this type + if perm.expirations[i].is_some() { + // remove it + perm.expirations[i] = None; + updated = true; + } + } + } + // if a change was made + if updated { + // if this address had no permissions to start + if !found { + perms.push(new_perm); + // if the last permission got revoked + } else if perm.expirations.iter().all(|&e| e.is_none()) { + perms.swap_remove(pos); + } + } + updated + } + + /// Returns a StdResult> list of ReceiveNft and BatchReceiveNft callacks that + /// should be done resulting from one Send + /// + /// # Arguments + /// + /// * `deps` - the contract's mutable external dependencies + /// * `contract_human` - a reference to the human address of the contract receiving the tokens + /// * `contract` - a reference to the canonical address of the contract receiving the tokens + /// * `receiver_info` - optional code hash and BatchReceiveNft implementation status of recipient contract + /// * `send_from_list` - list of SendFroms containing all the owners and their tokens being sent + /// * `msg` - a reference to the optional msg used to control ReceiveNft logic + /// * `sender` - a reference to the address that is sending the tokens + /// * `receivers` - a mutable reference the list of receiver contracts and their registration + /// info + #[allow(clippy::too_many_arguments)] + fn receiver_callback_msgs( + &self, + deps: &mut DepsMut, + _env: Env, + contract_human: &str, + contract: &CanonicalAddr, + receiver_info: Option, + send_from_list: Vec, + msg: &Option, + sender: &Addr, + receivers: &mut Vec, + ) -> StdResult> { + let (code_hash, impl_batch) = if let Some(supplied) = receiver_info { + ( + supplied.recipient_code_hash, + supplied.also_implements_batch_receive_nft.unwrap_or(false), + ) + } else if let Some(receiver) = receivers.iter().find(|&r| r.contract == *contract) { + ( + receiver.registration.code_hash.clone(), + receiver.registration.impl_batch, + ) + } else { + let store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_RECEIVERS); + let registration: ReceiveRegistration = may_load(&store, contract.as_slice())? + .unwrap_or(ReceiveRegistration { + code_hash: String::new(), + impl_batch: false, + }); + let receiver = CacheReceiverInfo { + contract: contract.clone(), + registration: registration.clone(), + }; + receivers.push(receiver); + (registration.code_hash, registration.impl_batch) + }; + if code_hash.is_empty() { + return Ok(Vec::new()); + } + let mut callbacks: Vec = Vec::new(); + for send_from in send_from_list.into_iter() { + // if BatchReceiveNft is implemented, use it + if impl_batch { + callbacks.push(batch_receive_nft_msg( + sender.clone(), + deps.api.addr_humanize(&send_from.owner)?, + send_from.token_ids, + msg.clone(), + code_hash.clone(), + contract_human.to_string(), + )?); + //otherwise do a bunch of BatchReceiveNft + } else { + for token_id in send_from.token_ids.into_iter() { + callbacks.push(receive_nft_msg( + deps.api.addr_humanize(&send_from.owner)?, + token_id, + msg.clone(), + code_hash.clone(), + contract_human.to_string(), + )?); + } + } + } + Ok(callbacks) + } + + /// Returns StdResult<()> + /// + /// update owners' inventories and AuthLists to reflect recent burns/transfers + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + /// * `updates` - a slice of an InventoryUpdate list to modify and store new inventories/AuthLists + /// * `num_perm_types` - the number of permission types + fn update_owner_inventory( + &self, + storage: &mut dyn Storage, + updates: &[InventoryUpdate], + num_perm_types: usize, + ) -> StdResult<()> { + for update in updates { + let owner_slice = update.inventory.owner.as_slice(); + // update the inventories + update.inventory.save(storage)?; + // update the AuthLists if tokens were lost + if !update.remove.is_empty() { + let mut auth_store = PrefixedStorage::new(storage, PREFIX_AUTHLIST); + let may_list: Option> = may_load(&auth_store, owner_slice)?; + if let Some(list) = may_list { + let mut new_list = Vec::new(); + for mut auth in list.into_iter() { + for i in 0..num_perm_types { + auth.tokens[i].retain(|t| !update.remove.contains(t)); + } + if !auth.tokens.iter().all(|u| u.is_empty()) { + new_list.push(auth) + } + } + if new_list.is_empty() { + remove(&mut auth_store, owner_slice); + } else { + save(&mut auth_store, owner_slice, &new_list)?; + } + } + } + } + Ok(()) + } + + /// Returns StdResult + /// + /// transfers a token, clears the token's permissions, and returns the previous owner's address + /// + /// # Arguments + /// + /// * `deps` - a mutable reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `config` - a mutable reference to the Config + /// * `sender` - a reference to the message sender address + /// * `token_id` - token id String of token being transferred + /// * `recipient` - the recipient's address + /// * `oper_for` - a mutable reference to a list of owners that gave the sender "all" permission + /// * `inv_updates` - a mutable reference to the list of token inventories to update + /// * `memo` - optional memo for the transfer tx + #[allow(clippy::too_many_arguments)] + fn transfer_impl( + &self, + deps: &mut DepsMut, + block: &BlockInfo, + config: &mut Config, + sender: &CanonicalAddr, + token_id: String, + recipient: CanonicalAddr, + oper_for: &mut Vec, + inv_updates: &mut Vec, + memo: Option, + ) -> StdResult { + let (mut token, idx) = self.get_token_if_permitted( + deps.as_ref(), + block, + &token_id, + Some(sender), + PermissionType::Transfer, + oper_for, + config, + )?; + if !token.transferable { + return Err(StdError::generic_err(format!( + "Token ID: {} is non-transferable", + token_id + ))); + } + let old_owner = token.owner; + // throw error if ownership would not change + if old_owner == recipient { + return Err(StdError::generic_err(format!( + "Attempting to transfer token ID: {} to the address that already owns it", + &token_id + ))); + } + token.owner = recipient.clone(); + token.permissions.clear(); + + let update_addrs = vec![recipient.clone(), old_owner.clone()]; + // save updated token info + let mut info_store = PrefixedStorage::new(deps.storage, PREFIX_INFOS); + json_save(&mut info_store, &idx.to_le_bytes(), &token)?; + // log the inventory changes + for addr in update_addrs.into_iter() { + let inv_upd = if let Some(inv) = + inv_updates.iter_mut().find(|i| i.inventory.owner == addr) + { + inv + } else { + let inventory = Inventory::new(deps.storage, addr)?; + let new_inv = InventoryUpdate { + inventory, + remove: HashSet::new(), + }; + inv_updates.push(new_inv); + inv_updates.last_mut().ok_or_else(|| { + StdError::generic_err("Just pushed an InventoryUpdate so this can not happen") + })? + }; + // if updating the recipient's inventory + if inv_upd.inventory.owner == recipient { + inv_upd.inventory.insert(deps.storage, idx, false)?; + // else updating the old owner's inventory + } else { + inv_upd.inventory.remove(deps.storage, idx, false)?; + inv_upd.remove.insert(idx); + } + } + + let sndr = if old_owner == *sender { + None + } else { + Some(sender.clone()) + }; + // store the tx + store_transfer( + deps.storage, + config, + block, + token_id, + old_owner.clone(), + sndr, + recipient, + memo, + )?; + Ok(old_owner) + } + + /// Returns StdResult> + /// + /// transfer or sends a list of tokens and returns a list of ReceiveNft callbacks if applicable + /// + /// # Arguments + /// + /// * `deps` - the contract's mutable external dependencies + /// * `env` - a reference to the Env of the contract's environment + /// * `msg_sender` - a reference to the message sender's address + /// * `config` - a mutable reference to the Config + /// * `transfers` - optional list of transfers to perform + /// * `sends` - optional list of sends to perform + fn send_list( + &self, + mut deps: DepsMut, + env: &Env, + msg_sender: &Addr, + config: &mut Config, + transfers: Option>, + sends: Option>, + ) -> StdResult> { + let mut messages: Vec = Vec::new(); + let mut oper_for: Vec = Vec::new(); + let mut inv_updates: Vec = Vec::new(); + let num_perm_types = PermissionType::ViewOwner.num_types(); + let sender = deps.api.addr_canonicalize(msg_sender.as_str())?; + if let Some(xfers) = transfers { + for xfer in xfers.into_iter() { + let recipient_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(&xfer.recipient)?.as_str())?; + for token_id in xfer.token_ids.into_iter() { + let _o = self.transfer_impl( + &mut deps, + &env.block, + config, + &sender, + token_id, + recipient_raw.clone(), + &mut oper_for, + &mut inv_updates, + xfer.memo.clone(), + )?; + } + } + } else if let Some(snds) = sends { + let mut receivers = Vec::new(); + for send in snds.into_iter() { + let contract_raw = deps + .api + .addr_canonicalize(deps.api.addr_validate(&send.contract)?.as_str())?; + let mut send_from_list: Vec = Vec::new(); + for token_id in send.token_ids.into_iter() { + let owner_raw = self.transfer_impl( + &mut deps, + &env.block, + config, + &sender, + token_id.clone(), + contract_raw.clone(), + &mut oper_for, + &mut inv_updates, + send.memo.clone(), + )?; + // compile list of all tokens being sent from each owner in this Send + if let Some(sd_fm) = send_from_list.iter_mut().find(|s| s.owner == owner_raw) { + sd_fm.token_ids.push(token_id.clone()); + } else { + let new_sd_fm = SendFrom { + owner: owner_raw, + token_ids: vec![token_id.clone()], + }; + send_from_list.push(new_sd_fm); + } + } + // get BatchReceiveNft and ReceiveNft msgs for all the tokens sent in this Send + messages.extend(self.receiver_callback_msgs( + &mut deps, + env.clone(), + &send.contract, + &contract_raw, + send.receiver_info, + send_from_list, + &send.msg, + msg_sender, + &mut receivers, + )?); + } + } + save(deps.storage, CONFIG_KEY, &config)?; + self.update_owner_inventory(deps.storage, &inv_updates, num_perm_types)?; + Ok(messages) + } + + /// Returns StdResult<()> + /// + /// burns a list of tokens + /// + /// # Arguments + /// + /// * `deps` - a mutable reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the current BlockInfo + /// * `config` - a mutable reference to the Config + /// * `sender` - a reference to the message sender address + /// * `burns` - list of burns to perform + fn burn_list( + &self, + deps: DepsMut, + block: &BlockInfo, + config: &mut Config, + sender: &CanonicalAddr, + burns: Vec, + ) -> StdResult<()> { + let mut oper_for: Vec = Vec::new(); + let mut inv_updates: Vec = Vec::new(); + let num_perm_types = PermissionType::ViewOwner.num_types(); + for burn in burns.into_iter() { + for token_id in burn.token_ids.into_iter() { + let (token, idx) = self.get_token_if_permitted( + deps.as_ref(), + block, + &token_id, + Some(sender), + PermissionType::Transfer, + &mut oper_for, + config, + )?; + if !config.burn_is_enabled && token.transferable { + return Err(StdError::generic_err( + "Burn functionality is not enabled for this token", + )); + } + // log the inventory change + let inv_upd = if let Some(inv) = inv_updates + .iter_mut() + .find(|i| i.inventory.owner == token.owner) + { + inv + } else { + let inventory = Inventory::new(deps.storage, token.owner.clone())?; + let new_inv = InventoryUpdate { + inventory, + remove: HashSet::new(), + }; + inv_updates.push(new_inv); + inv_updates.last_mut().ok_or_else(|| { + StdError::generic_err( + "Just pushed an InventoryUpdate so this can not happen", + ) + })? + }; + inv_upd.inventory.remove(deps.storage, idx, false)?; + inv_upd.remove.insert(idx); + let token_key = idx.to_le_bytes(); + // decrement token count + config.token_cnt = config.token_cnt.saturating_sub(1); + // remove from maps + let mut map2idx = PrefixedStorage::new(deps.storage, PREFIX_MAP_TO_INDEX); + remove(&mut map2idx, token_id.as_bytes()); + let mut map2id = PrefixedStorage::new(deps.storage, PREFIX_MAP_TO_ID); + remove(&mut map2id, &token_key); + // remove the token info + let mut info_store = PrefixedStorage::new(deps.storage, PREFIX_INFOS); + remove(&mut info_store, &token_key); + // remove metadata if existent + let mut pub_store = PrefixedStorage::new(deps.storage, PREFIX_PUB_META); + remove(&mut pub_store, &token_key); + let mut priv_store = PrefixedStorage::new(deps.storage, PREFIX_PRIV_META); + remove(&mut priv_store, &token_key); + // remove mint run info if existent + let mut run_store = PrefixedStorage::new(deps.storage, PREFIX_MINT_RUN); + remove(&mut run_store, &token_key); + // remove royalty info if existent + let mut roy_store = PrefixedStorage::new(deps.storage, PREFIX_ROYALTY_INFO); + remove(&mut roy_store, &token_key); + + let brnr = if token.owner == *sender { + None + } else { + Some(sender.clone()) + }; + // store the tx + store_burn( + deps.storage, + config, + block, + token_id, + token.owner, + brnr, + burn.memo.clone(), + )?; + } + } + save(deps.storage, CONFIG_KEY, &config)?; + self.update_owner_inventory(deps.storage, &inv_updates, num_perm_types)?; + Ok(()) + } + + /// Returns > + /// + /// mints a list of new tokens and returns the ids of the tokens minted + /// + /// # Arguments + /// + /// * `deps` - a mutable reference to Extern containing all the contract's external dependencies + /// * `env` - a reference to the Env of the contract's environment + /// * `config` - a mutable reference to the Config + /// * `sender_raw` - a reference to the message sender address + /// * `mints` - list of mints to perform + fn mint_list( + &self, + deps: DepsMut, + env: &Env, + config: &mut Config, + sender_raw: &CanonicalAddr, + mints: Vec>, + ) -> StdResult> { + let mut inventories: Vec = Vec::new(); + let mut minted: Vec = Vec::new(); + let default_roy: Option = may_load(deps.storage, DEFAULT_ROYALTY_KEY)?; + for mint in mints.into_iter() { + let id = mint.token_id.unwrap_or(format!("{}", config.mint_cnt)); + // check if id already exists + let mut map2idx = PrefixedStorage::new(deps.storage, PREFIX_MAP_TO_INDEX); + let may_exist: Option = may_load(&map2idx, id.as_bytes())?; + if may_exist.is_some() { + return Err(StdError::generic_err(format!( + "Token ID {} is already in use", + id + ))); + } + // increment token count + config.token_cnt = config.token_cnt.checked_add(1).ok_or_else(|| { + StdError::generic_err( + "Attempting to mint more tokens than the implementation limit", + ) + })?; + // map new token id to its index + save(&mut map2idx, id.as_bytes(), &config.mint_cnt)?; + let recipient = if let Some(o) = mint.owner { + deps.api + .addr_canonicalize(deps.api.addr_validate(&o)?.as_str())? + } else { + sender_raw.clone() + }; + let transferable = mint.transferable.unwrap_or(true); + let token = Token { + owner: recipient.clone(), + permissions: Vec::new(), + unwrapped: !config.sealed_metadata_is_enabled, + transferable, + }; + + // save new token info and token_Extension info + let token_key = config.mint_cnt.to_le_bytes(); + let mut info_store = PrefixedStorage::new(deps.storage, PREFIX_INFOS); + json_save(&mut info_store, &token_key, &token)?; + self.token_extension_info + .insert(deps.storage, &id, &mint.extension)?; + // add token to owner's list + let inventory = + if let Some(inv) = inventories.iter_mut().find(|i| i.owner == token.owner) { + inv + } else { + let new_inv = Inventory::new(deps.storage, token.owner.clone())?; + inventories.push(new_inv); + inventories.last_mut().ok_or_else(|| { + StdError::generic_err("Just pushed an Inventory so this can not happen") + })? + }; + inventory.insert(deps.storage, config.mint_cnt, false)?; + + // map index to id + let mut map2id = PrefixedStorage::new(deps.storage, PREFIX_MAP_TO_ID); + save(&mut map2id, &token_key, &id)?; + + // + // If you wanted to store an additional data struct for each NFT, you would create + // a new prefix and store with the `token_key` like below + // + // save the metadata + if let Some(pub_meta) = mint.public_metadata { + self.enforce_metadata_field_exclusion(&pub_meta)?; + let mut pub_store = PrefixedStorage::new(deps.storage, PREFIX_PUB_META); + save(&mut pub_store, &token_key, &pub_meta)?; + self.pub_metadata.insert(deps.storage, &id, &pub_meta)?; + } + if let Some(priv_meta) = mint.private_metadata { + self.enforce_metadata_field_exclusion(&priv_meta)?; + let mut priv_store = PrefixedStorage::new(deps.storage, PREFIX_PRIV_META); + save(&mut priv_store, &token_key, &priv_meta)?; + self.priv_metadata.insert(deps.storage, &id, &priv_meta)?; + } + // save the mint run info + let (mint_run, serial_number, quantity_minted_this_run) = + if let Some(ser) = mint.serial_number { + ( + ser.mint_run, + Some(ser.serial_number), + ser.quantity_minted_this_run, + ) + } else { + (None, None, None) + }; + let mint_info = StoredMintRunInfo { + token_creator: sender_raw.clone(), + time_of_minting: env.block.time.seconds(), + mint_run, + serial_number, + quantity_minted_this_run, + }; + let mut run_store = PrefixedStorage::new(deps.storage, PREFIX_MINT_RUN); + save(&mut run_store, &token_key, &mint_info)?; + // check/save royalty information only if the token is transferable + if token.transferable { + let mut roy_store = PrefixedStorage::new(deps.storage, PREFIX_ROYALTY_INFO); + self.store_royalties( + &mut roy_store, + deps.api, + mint.royalty_info.as_ref(), + default_roy.as_ref(), + &token_key, + )?; + } + // + // + + // store the tx + store_mint( + deps.storage, + config, + &env.block, + id.clone(), + sender_raw.clone(), + recipient, + mint.memo, + )?; + minted.push(id); + // increment index for next mint + config.mint_cnt = config.mint_cnt.checked_add(1).ok_or_else(|| { + StdError::generic_err("Attempting to mint more times than the implementation limit") + })?; + } + // save all the updated inventories + for inventory in inventories.iter() { + inventory.save(deps.storage)?; + } + save(deps.storage, CONFIG_KEY, &config)?; + Ok(minted) + } + + /// Returns StdResult<()> + /// + /// verifies the royalty information is valid and if so, stores the royalty info for the token + /// or as default + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the storage for this RoyaltyInfo + /// * `api` - a reference to the Api used to convert human and canonical addresses + /// * `royalty_info` - an optional reference to the RoyaltyInfo to store + /// * `default` - an optional reference to the default StoredRoyaltyInfo to use if royalty_info is + /// not provided + /// * `key` - the storage key (either token key or default key) + fn store_royalties( + &self, + storage: &mut dyn Storage, + api: &dyn Api, + royalty_info: Option<&RoyaltyInfo>, + default: Option<&StoredRoyaltyInfo>, + key: &[u8], + ) -> StdResult<()> { + // if RoyaltyInfo is provided, check and save it + if let Some(royal_inf) = royalty_info { + // the allowed message length won't let enough u16 rates to overflow u128 + let total_rates: u128 = royal_inf.royalties.iter().map(|r| r.rate as u128).sum(); + let (royalty_den, overflow) = + U256::from(10).overflowing_pow(U256::from(royal_inf.decimal_places_in_rates)); + if overflow { + return Err(StdError::generic_err( + "The number of decimal places used in the royalty rates is larger than supported", + )); + } + if U256::from(total_rates) > royalty_den { + return Err(StdError::generic_err( + "The sum of royalty rates must not exceed 100%", + )); + } + let stored = royal_inf.to_stored(api)?; + save(storage, key, &stored) + } else if let Some(def) = default { + save(storage, key, def) + } else { + remove(storage, key); + Ok(()) + } + } + + /// Returns StdResult<()> + /// + /// makes sure that Metadata does not have both `token_uri` and `extension` + /// + /// # Arguments + /// + /// * `metadata` - a reference to Metadata + fn enforce_metadata_field_exclusion(&self, metadata: &Metadata) -> StdResult<()> { + if metadata.token_uri.is_some() && metadata.extension.is_some() { + return Err(StdError::generic_err( + "Metadata can not have BOTH token_uri AND extension", + )); + } + Ok(()) + } + + /// Returns StdResult> from determining the querying address (if possible) either + /// from a permit validation or a ViewerInfo + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `viewer` - optional address and key making an authenticated query request + /// * `from_permit` - the address derived from an Owner permit, if applicable + fn get_querier( + &self, + deps: Deps, + viewer: Option, + from_permit: Option, + ) -> StdResult> { + if from_permit.is_some() { + return Ok(from_permit); + } + let viewer_raw = viewer + .map(|v| { + let addr = deps.api.addr_validate(&v.address)?; + let raw = deps.api.addr_canonicalize(addr.as_str())?; + ViewingKey::check(deps.storage, addr.as_str(), &v.viewing_key) + .map_err(|_| StdError::generic_err(VIEWING_KEY_ERR_MSG))?; + Ok::(raw) + }) + .transpose()?; + Ok(viewer_raw) + } + + /// Returns StdResult> of all the token information the querier is permitted to + /// view for multiple tokens. This may include the owner, the public metadata, the private metadata, royalty + /// information, mint run information, whether the token is unwrapped, whether the token is + /// transferable, and the token and inventory approvals + /// + /// # Arguments + /// + /// * `deps` - a reference to Extern containing all the contract's external dependencies + /// * `block` - a reference to the BlockInfo + /// * `token_ids` - list of token ids to retrieve the info of + /// * `viewer` - optional address and key making an authenticated query request + /// * `include_expired` - optionally true if the Approval lists should include expired Approvals + /// * `from_permit` - address derived from an Owner permit, if applicable + pub fn dossier_list( + &self, + deps: Deps, + block: &BlockInfo, + token_ids: Vec, + viewer: Option, + include_expired: Option, + from_permit: Option, + ) -> StdResult> { + let viewer_raw = self.get_querier(deps, viewer, from_permit)?; + let opt_viewer = viewer_raw.as_ref(); + let incl_exp = include_expired.unwrap_or(false); + let config: Config = load(deps.storage, CONFIG_KEY)?; + let contract_creator = deps + .api + .addr_humanize(&load::(deps.storage, CREATOR_KEY)?)?; + + let perm_type_info = PermissionTypeInfo { + view_owner_idx: PermissionType::ViewOwner.to_usize(), + view_meta_idx: PermissionType::ViewMetadata.to_usize(), + transfer_idx: PermissionType::Transfer.to_usize(), + num_types: PermissionType::Transfer.num_types(), + }; + // used to shortcut permission checks if the viewer is already a known operator for a list of owners + let mut owner_oper_for: Vec = Vec::new(); + let mut meta_oper_for: Vec = Vec::new(); + let mut xfer_oper_for: Vec = Vec::new(); + let mut owner_cache: Vec = Vec::new(); + let mut dossiers: Vec = Vec::new(); + // set up all the immutable storage references + let own_priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_OWNER_PRIV); + let pub_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_PUB_META); + let priv_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_PRIV_META); + let roy_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ROYALTY_INFO); + let run_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_MINT_RUN); + let all_store = ReadonlyPrefixedStorage::new(deps.storage, PREFIX_ALL_PERMISSIONS); + + for id in token_ids.into_iter() { + let err_msg = format!( + "You are not authorized to perform this action on token {}", + &id + ); + // if token supply is private, don't leak that the token id does not exist + // instead just say they are not authorized for that token + let opt_err = if config.token_supply_is_public { + None + } else { + Some(&*err_msg) + }; + let (mut token, idx) = self.get_token(deps.storage, &id, opt_err)?; + let owner_slice = token.owner.as_slice(); + // get the owner info either from the cache or storage + let owner_inf = if let Some(inf) = owner_cache.iter().find(|o| o.owner == token.owner) { + inf + } else { + let owner_is_public: bool = + may_load(&own_priv_store, owner_slice)?.unwrap_or(config.owner_is_public); + let mut all_perm: Vec = + json_may_load(&all_store, owner_slice)?.unwrap_or_default(); + let (inventory_approvals, view_owner_exp, view_meta_exp) = self + .gen_snip721_approvals( + deps.api, + block, + &mut all_perm, + incl_exp, + &perm_type_info, + )?; + owner_cache.push(OwnerInfo { + owner: token.owner.clone(), + owner_is_public, + inventory_approvals, + view_owner_exp, + view_meta_exp, + }); + owner_cache.last().ok_or_else(|| { + StdError::generic_err("This can't happen since we just pushed an OwnerInfo!") + })? + }; + let global_pass = owner_inf.owner_is_public; + // get the owner if permitted + let owner = if global_pass + || self + .check_perm_core( + deps, + block, + &token, + &id, + opt_viewer, + owner_slice, + perm_type_info.view_owner_idx, + &mut owner_oper_for, + &err_msg, + ) + .is_ok() + { + Some(deps.api.addr_humanize(&token.owner)?) + } else { + None + }; + // get the public metadata + let token_key = idx.to_le_bytes(); + let public_metadata: Option = may_load(&pub_store, &token_key)?; + // get the private metadata if it is not sealed and if the viewer is permitted + let mut display_private_metadata_error = None; + let private_metadata = if let Err(err) = self.check_perm_core( + deps, + block, + &token, + &id, + opt_viewer, + owner_slice, + perm_type_info.view_meta_idx, + &mut meta_oper_for, + &err_msg, + ) { + if let StdError::GenericErr { msg, .. } = err { + display_private_metadata_error = Some(msg); + } + None + } else if !token.unwrapped { + display_private_metadata_error = Some(format!( + "Sealed metadata of token {} must be unwrapped by calling Reveal before it can be viewed", &id + )); + None + } else { + let priv_meta: Option = may_load(&priv_store, &token_key)?; + priv_meta + }; + // get the royalty information if present + let may_roy_inf: Option = may_load(&roy_store, &token_key)?; + let royalty_info = may_roy_inf + .map(|r| { + let hide_addr = self + .check_perm_core( + deps, + block, + &token, + &id, + opt_viewer, + owner_slice, + perm_type_info.transfer_idx, + &mut xfer_oper_for, + &err_msg, + ) + .is_err(); + r.to_human(deps.api, hide_addr) + }) + .transpose()?; + // get the mint run information + let mint_run: StoredMintRunInfo = load(&run_store, &token_key)?; + // get the token approvals + let (token_approv, token_owner_exp, token_meta_exp) = self.gen_snip721_approvals( + deps.api, + block, + &mut token.permissions, + incl_exp, + &perm_type_info, + )?; + // determine if ownership is public + let (public_ownership_expiration, owner_is_public) = if global_pass { + (Some(Expiration::Never), true) + } else if token_owner_exp.is_some() { + (token_owner_exp, true) + } else { + ( + owner_inf.view_owner_exp.as_ref().cloned(), + owner_inf.view_owner_exp.is_some(), + ) + }; + // determine if private metadata is public + let (private_metadata_is_public_expiration, private_metadata_is_public) = + if token_meta_exp.is_some() { + (token_meta_exp, true) + } else { + ( + owner_inf.view_meta_exp.as_ref().cloned(), + owner_inf.view_meta_exp.is_some(), + ) + }; + // if the viewer is the owner, display the approvals + let (token_approvals, inventory_approvals) = opt_viewer.map_or((None, None), |v| { + if token.owner == *v { + ( + Some(token_approv), + Some(owner_inf.inventory_approvals.clone()), + ) + } else { + (None, None) + } + }); + dossiers.push(BatchNftDossierElement { + token_id: id, + owner, + public_metadata, + private_metadata, + royalty_info, + mint_run_info: Some(mint_run.to_human(deps.api, contract_creator.clone())?), + transferable: token.transferable, + unwrapped: token.unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + }); + } + Ok(dossiers) + } +} diff --git a/contracts/external/snip721-roles-impl/src/expiration.rs b/contracts/external/snip721-roles-impl/src/expiration.rs new file mode 100644 index 0000000..238774a --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/expiration.rs @@ -0,0 +1,86 @@ +use std::fmt; + +use cosmwasm_std::BlockInfo; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +/// at the given point in time and after, Expiration will be considered expired +pub enum Expiration { + /// expires at this block height + AtHeight(u64), + /// expires at the time in seconds since 01/01/1970 + AtTime(u64), + /// never expires + Never, +} + +impl fmt::Display for Expiration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Expiration::AtHeight(height) => write!(f, "expiration height: {}", height), + Expiration::AtTime(time) => write!(f, "expiration time: {}", time), + Expiration::Never => write!(f, "expiration: never"), + } + } +} + +/// default is Never +impl Default for Expiration { + fn default() -> Self { + Expiration::Never + } +} + +impl Expiration { + /// Returns bool, true if Expiration has expired + /// + /// # Arguments + /// + /// * `block` - a reference to the BlockInfo containing the time to compare the Expiration to + pub fn is_expired(&self, block: &BlockInfo) -> bool { + match self { + Expiration::AtHeight(height) => block.height >= *height, + Expiration::AtTime(time) => block.time.seconds() >= *time, + Expiration::Never => false, + } + } +} + +#[cfg(test)] +mod test { + use cosmwasm_std::Timestamp; + + use super::*; + + #[test] + fn test_expiration() { + let block_h1000_t1000000 = BlockInfo { + height: 1000, + time: Timestamp::from_seconds(1000000), + chain_id: "test".to_string(), + random: None, + }; + + let block_h2000_t2000000 = BlockInfo { + height: 2000, + time: Timestamp::from_seconds(2000000), + chain_id: "test".to_string(), + random: None, + }; + let exp_h1000 = Expiration::AtHeight(1000); + let exp_t1000000 = Expiration::AtTime(1000000); + let exp_h1500 = Expiration::AtHeight(1500); + let exp_t1500000 = Expiration::AtTime(1500000); + let exp_never = Expiration::default(); + + assert!(exp_h1000.is_expired(&block_h1000_t1000000)); + assert!(!exp_h1500.is_expired(&block_h1000_t1000000)); + assert!(exp_h1500.is_expired(&block_h2000_t2000000)); + assert!(!exp_never.is_expired(&block_h2000_t2000000)); + assert!(exp_t1000000.is_expired(&block_h1000_t1000000)); + assert!(!exp_t1500000.is_expired(&block_h1000_t1000000)); + assert!(exp_t1500000.is_expired(&block_h2000_t2000000)); + } +} diff --git a/contracts/external/snip721-roles-impl/src/inventory.rs b/contracts/external/snip721-roles-impl/src/inventory.rs new file mode 100644 index 0000000..1c07340 --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/inventory.rs @@ -0,0 +1,269 @@ +use std::collections::HashSet; + +use cosmwasm_std::{CanonicalAddr, StdError, StdResult, Storage}; +use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage}; +use serde::{Deserialize, Serialize}; + +use crate::state::{may_load, remove, save}; + +/// storage prefix for an owner's inventory count +pub const PREFIX_INVENTORY_COUNT: &[u8] = b"invcnt"; +/// storage prefix for mapping a token idx to an inventory index +pub const PREFIX_INVENTORY_MAP: &[u8] = b"invmap"; +/// storage prefix for a token idx in the inventory +pub const PREFIX_INVENTORY_TOKEN: &[u8] = b"invtok"; + +/// token inventory +#[derive(Serialize, Deserialize)] +pub struct Inventory { + /// owner's address + pub owner: CanonicalAddr, + /// number of tokens in the inventory + pub cnt: u32, +} + +impl Inventory { + /// Returns StdResult + /// + /// creates a new Inventory by loading it from storage or creating a new one + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + /// * `owner` - the owner's address + pub fn new(storage: &dyn Storage, owner: CanonicalAddr) -> StdResult { + let store = ReadonlyPrefixedStorage::new(storage, PREFIX_INVENTORY_COUNT); + + Ok(Inventory { + cnt: may_load::(&store, owner.as_slice())?.unwrap_or(0), + owner, + }) + } + + /// Returns StdResult<()> + /// + /// adds a token to the inventory + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + /// * `token_idx` - the token's idx + /// * `save_cnt` - true if the inventory count should be saved + pub fn insert( + &mut self, + storage: &mut dyn Storage, + token_idx: u32, + save_cnt: bool, + ) -> StdResult<()> { + let owner_slice = self.owner.as_slice(); + let mut map_store = + PrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_MAP, owner_slice]); + let token_key = token_idx.to_le_bytes(); + // nothing to do if the token idx is already in the inventory + if may_load::(&map_store, &token_key)?.is_some() { + return Ok(()); + } + // map the new token to the top index + save(&mut map_store, &token_key, &self.cnt)?; + // save the token idx + let mut token_store = + PrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_TOKEN, owner_slice]); + save(&mut token_store, &self.cnt.to_le_bytes(), &token_idx)?; + // increment the count + self.cnt = self.cnt.checked_add(1).ok_or_else(|| { + StdError::generic_err( + "This would put your token count above the amount supported by the contract", + ) + })?; + // save the count if desired + if save_cnt { + let mut cnt_store = PrefixedStorage::new(storage, PREFIX_INVENTORY_COUNT); + save(&mut cnt_store, owner_slice, &self.cnt)?; + } + Ok(()) + } + + /// Returns StdResult<()> + /// + /// removes a token from the inventory + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + /// * `token_idx` - the token's idx + /// * `save_cnt` - true if the inventory count should be saved + pub fn remove( + &mut self, + storage: &mut dyn Storage, + token_idx: u32, + save_cnt: bool, + ) -> StdResult<()> { + let owner_slice = self.owner.as_slice(); + let mut map_store = + PrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_MAP, owner_slice]); + let token_key = token_idx.to_le_bytes(); + // if the token is in the inventory + if let Some(inv_idx) = may_load::(&map_store, &token_key)? { + // remove it from the map + remove(&mut map_store, &token_key); + // decrement the count + self.cnt = self.cnt.saturating_sub(1); + // if it was not the last element + if inv_idx != self.cnt { + // get the last token + let mut token_store = + PrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_TOKEN, owner_slice]); + let last_tkn: u32 = may_load(&token_store, &self.cnt.to_le_bytes())? + .ok_or_else(|| StdError::generic_err("Inventory token storage is corrupt"))?; + // swap the last token to the position of the removed token + save(&mut token_store, &inv_idx.to_le_bytes(), &last_tkn)?; + // change the previous last token's mapping + let mut map_store = + PrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_MAP, owner_slice]); + save(&mut map_store, &last_tkn.to_le_bytes(), &inv_idx)?; + } + + // save the count if desired + if save_cnt { + let mut cnt_store = PrefixedStorage::new(storage, PREFIX_INVENTORY_COUNT); + save(&mut cnt_store, owner_slice, &self.cnt)?; + } + } + Ok(()) + } + + /// Returns StdResult> + /// + /// creates a HashSet from the Inventory + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn to_set(&self, storage: &dyn Storage) -> StdResult> { + let mut set: HashSet = HashSet::new(); + let token_store = ReadonlyPrefixedStorage::multilevel( + storage, + &[PREFIX_INVENTORY_TOKEN, self.owner.as_slice()], + ); + for idx in 0..self.cnt { + let token_idx: u32 = may_load(&token_store, &idx.to_le_bytes())? + .ok_or_else(|| StdError::generic_err("Inventory token storage is corrupt"))?; + set.insert(token_idx); + } + Ok(set) + } + + /// Returns StdResult + /// + /// returns true if the inventory contains the input token_idx + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + /// * `token_idx` - the token index in question + pub fn contains(&self, storage: &dyn Storage, token_idx: u32) -> StdResult { + let map_store = ReadonlyPrefixedStorage::multilevel( + storage, + &[PREFIX_INVENTORY_MAP, self.owner.as_slice()], + ); + Ok(may_load::(&map_store, &token_idx.to_le_bytes())?.is_some()) + } + + /// Returns StdResult + /// + /// returns true if the input address owns the input token_idx + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + /// * `owner` - a reference to the presumed owner's address + /// * `token_idx` - the token index in question + pub fn owns(storage: &dyn Storage, owner: &CanonicalAddr, token_idx: u32) -> StdResult { + let map_store = + ReadonlyPrefixedStorage::multilevel(storage, &[PREFIX_INVENTORY_MAP, owner.as_slice()]); + Ok(may_load::(&map_store, &token_idx.to_le_bytes())?.is_some()) + } + + /// Returns StdResult<()> + /// + /// saves the inventory count + /// + /// # Arguments + /// + /// * `storage` - a mutable reference to the contract's storage + pub fn save(&self, storage: &mut dyn Storage) -> StdResult<()> { + let mut cnt_store = PrefixedStorage::new(storage, PREFIX_INVENTORY_COUNT); + save(&mut cnt_store, self.owner.as_slice(), &self.cnt) + } +} + +/// an "iterator" (deos not implement the trait) over an Inventory +pub struct InventoryIter<'a> { + /// a reference to the Inventory to iterate + pub inventory: &'a Inventory, + /// the current index to retrieve + pub curr: u32, +} + +impl<'a> InventoryIter<'a> { + /// Returns InventoryIter + /// + /// creates a new InventoryIter + /// + /// # Arguments + /// + /// * `inventory` - a reference to the Inventory to iterate over + pub fn new(inventory: &'a Inventory) -> Self { + InventoryIter { inventory, curr: 0 } + } + + /// Returns StdResult + /// + /// creates an InventoryIter that starts after the supplied token index + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + /// * `inventory` - a reference to the Inventory to iterate over + /// * `token_idx` - optional token_index to start iterating after + /// * `err_msg` - error message if the token index is not in the inventory + pub fn start_after( + storage: &dyn Storage, + inventory: &'a Inventory, + token_idx: u32, + err_msg: &str, + ) -> StdResult { + let map_store = ReadonlyPrefixedStorage::multilevel( + storage, + &[PREFIX_INVENTORY_MAP, inventory.owner.as_slice()], + ); + let mut curr = may_load::(&map_store, &token_idx.to_le_bytes())? + .ok_or_else(|| StdError::generic_err(err_msg))?; + curr = curr.saturating_add(1); + Ok(InventoryIter { inventory, curr }) + } + + /// Returns StdResult> + /// + /// returns the next token_idx in the inventory + /// + /// # Arguments + /// + /// * `storage` - a reference to the contract's storage + pub fn next(&mut self, storage: &dyn Storage) -> StdResult> { + // at the end + if self.curr >= self.inventory.cnt { + return Ok(None); + } + let token_store = ReadonlyPrefixedStorage::multilevel( + storage, + &[PREFIX_INVENTORY_TOKEN, self.inventory.owner.as_slice()], + ); + let this = self.curr; + // bump the position + self.curr = self.curr.saturating_add(1); + may_load::(&token_store, &this.to_le_bytes())? + .ok_or_else(|| StdError::generic_err("Inventory token storage is corrupt")) + .map(Some) + } +} diff --git a/contracts/external/snip721-roles-impl/src/lib.rs b/contracts/external/snip721-roles-impl/src/lib.rs new file mode 100644 index 0000000..0fec9ec --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/lib.rs @@ -0,0 +1,16 @@ +//#![allow(clippy::field_reassign_with_default)] +pub mod contract; +pub mod expiration; +mod inventory; +pub mod mint_run; +pub mod msg; +pub mod receiver; +pub mod royalties; +pub mod state; +pub mod token; +// mod unittest_handles; +mod unittest_inventory; +// mod unittest_mint_run; +// mod unittest_non_transferable; +// mod unittest_queries; +// mod unittest_royalties; diff --git a/contracts/external/snip721-roles-impl/src/mint_run.rs b/contracts/external/snip721-roles-impl/src/mint_run.rs new file mode 100644 index 0000000..9675e2b --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/mint_run.rs @@ -0,0 +1,83 @@ +use cosmwasm_std::{Addr, Api, CanonicalAddr, StdResult}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// information about the minting of the NFT +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct MintRunInfo { + /// optional address of the SNIP-721 contract creator + pub collection_creator: Option, + /// optional address of this NFT's creator + pub token_creator: Option, + /// optional time of minting (in seconds since 01/01/1970) + pub time_of_minting: Option, + /// optional number of the mint run this token was minted in. A mint run represents a + /// batch of NFTs released at the same time. So if a creator decided to make 100 copies + /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and + /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all + /// be part of mint run number 2. + pub mint_run: Option, + /// optional serial number in this mint run. This is used to serialize + /// identical NFTs + pub serial_number: Option, + /// optional total number of NFTs minted on this run. This is used to + /// represent that this token is number m of n + pub quantity_minted_this_run: Option, +} + +/// stored information about the minting of the NFT +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct StoredMintRunInfo { + /// address of this NFT's creator + pub token_creator: CanonicalAddr, + /// optional time of minting (in seconds since 01/01/1970) + pub time_of_minting: u64, + /// optional number of the mint run this token was minted in. A mint run represents a + /// batch of NFTs released at the same time. So if a creator decided to make 100 copies + /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and + /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all + /// be part of mint run number 2. + pub mint_run: Option, + /// optional serial number in this mint run. This is used to serialize + /// identical NFTs + pub serial_number: Option, + /// optional total number of NFTs minted on this run. This is used to + /// represent that this token is number m of n + pub quantity_minted_this_run: Option, +} + +impl StoredMintRunInfo { + /// Returns StdResult from creating a MintRunInfo from a StoredMintRunInfo + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + /// * `contract_creator` - the address that instantiated the contract + pub fn to_human(&self, api: &dyn Api, contract_creator: Addr) -> StdResult { + Ok(MintRunInfo { + collection_creator: Some(contract_creator), + token_creator: Some(api.addr_humanize(&self.token_creator)?), + time_of_minting: Some(self.time_of_minting), + mint_run: self.mint_run, + serial_number: self.serial_number, + quantity_minted_this_run: self.quantity_minted_this_run, + }) + } +} + +/// Serial number to give an NFT when minting +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct SerialNumber { + /// optional number of the mint run this token will be minted in. A mint run represents a + /// batch of NFTs released at the same time. So if a creator decided to make 100 copies + /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and + /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all + /// be part of mint run number 2. + pub mint_run: Option, + /// serial number (in this mint run). This is used to serialize + /// identical NFTs + pub serial_number: u32, + /// optional total number of NFTs minted on this run. This is used to + /// represent that this token is number m of n + pub quantity_minted_this_run: Option, +} diff --git a/contracts/external/snip721-roles/src/snip721.rs b/contracts/external/snip721-roles-impl/src/msg.rs similarity index 78% rename from contracts/external/snip721-roles/src/snip721.rs rename to contracts/external/snip721-roles-impl/src/msg.rs index 6f92808..bee4b87 100644 --- a/contracts/external/snip721-roles/src/snip721.rs +++ b/contracts/external/snip721-roles-impl/src/msg.rs @@ -1,18 +1,20 @@ #![allow(clippy::large_enum_variant)] -use std::fmt; - -use cosmwasm_std::{Addr, Api, Binary, BlockInfo, CanonicalAddr, Coin, StdResult}; +use cosmwasm_schema::QueryResponses; +use cosmwasm_std::{Addr, Binary, Coin}; use schemars::JsonSchema; -use secret_toolkit::{ - permit::Permit, - utils::{HandleCallback, InitCallback}, -}; +use secret_toolkit::permit::Permit; use serde::{Deserialize, Serialize}; +use shade_protocol::utils::asset::RawContract; + +use crate::expiration::Expiration; +use crate::mint_run::{MintRunInfo, SerialNumber}; +use crate::royalties::{DisplayRoyaltyInfo, RoyaltyInfo}; +use crate::token::{Extension, Metadata}; /// Instantiation message -#[derive(Serialize, Deserialize, JsonSchema)] -pub struct Snip721InstantiateMsg { +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +pub struct InstantiateMsg { /// name of token contract pub name: String, /// token contract symbol @@ -31,6 +33,13 @@ pub struct Snip721InstantiateMsg { /// contract that instantiated it, but it could be used to execute any /// contract pub post_init_callback: Option, + pub query_auth: RawContract, +} + +#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] +pub struct InstantiateResponse { + pub contract_address: Addr, + pub code_hash: String, } /// This type represents optional configuration values. @@ -101,7 +110,7 @@ pub struct PostInstantiateCallback { #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[serde(rename_all = "snake_case")] -pub enum Snip721ExecuteMsg { +pub enum ExecuteMsg { /// mint new token MintNft { /// optional token id. if omitted, use current token index @@ -123,11 +132,13 @@ pub enum Snip721ExecuteMsg { memo: Option, /// optional message length padding padding: Option, + /// Any custom extension used by this contract + extension: MetadataExt, }, /// Mint multiple tokens BatchMintNft { /// list of mint operations to perform - mints: Vec, + mints: Vec>, /// optional message length padding padding: Option, }, @@ -157,6 +168,8 @@ pub enum Snip721ExecuteMsg { memo: Option, /// optional message length padding padding: Option, + /// Any custom extension used by this contract + extension: MetadataExt, }, /// set the public and/or private metadata. This can be called by either the token owner or /// a valid minter if they have been given this power by the appropriate config values @@ -404,10 +417,8 @@ pub enum Snip721ExecuteMsg { /// optional message length padding padding: Option, }, -} - -impl HandleCallback for Snip721ExecuteMsg { - const BLOCK_SIZE: usize = 256; + /// Extension msg + Extension { msg: ExecuteExt }, } /// permission access level @@ -426,7 +437,7 @@ pub enum AccessLevel { /// token mint info used when doing a BatchMint #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] -pub struct Mint { +pub struct Mint { /// optional token id, if omitted, use current token index pub token_id: Option, /// optional owner address, owned by the minter otherwise @@ -444,6 +455,8 @@ pub struct Mint { pub transferable: Option, /// optional memo for the tx pub memo: Option, + /// You can add any custom metadata here when you extend snip721-roles-base + pub extension: MetadataExt, } /// token burn info used when doing a BatchBurnNft @@ -483,7 +496,7 @@ pub struct Send { #[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] -pub enum Snip721ExecuteAnswer { +pub enum ExecuteAnswer { /// MintNft will also display the minted token's ID in the log attributes under the /// key `minted` in case minting was done as a callback message MintNft { @@ -647,15 +660,24 @@ pub struct Tx { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum Snip721QueryMsg { +#[derive(QueryResponses)] + +pub enum QueryMsg +where + QueryExt: JsonSchema, +{ /// display the contract's name and symbol + #[returns(QueryAnswer)] ContractInfo {}, /// display the contract's configuration + #[returns(QueryAnswer)] ContractConfig {}, /// display the list of authorized minters + #[returns(QueryAnswer)] Minters {}, /// display the number of tokens controlled by the contract. The token supply must /// either be public, or the querier must be an authenticated minter + #[returns(QueryAnswer)] NumTokens { /// optional address and key requesting to view the number of tokens viewer: Option, @@ -663,6 +685,7 @@ pub enum Snip721QueryMsg { /// display an optionally paginated list of all the tokens controlled by the contract. /// The token supply must either be public, or the querier must be an authenticated /// minter + #[returns(QueryAnswer)] AllTokens { /// optional address and key requesting to view the list of tokens viewer: Option, @@ -675,6 +698,7 @@ pub enum Snip721QueryMsg { /// is also the token's owner, the response will also include a list of any addresses /// that can transfer this token. The transfer approval list is for CW721 compliance, /// but the NftDossier query will be more complete by showing viewing approvals as well + #[returns(QueryAnswer)] OwnerOf { token_id: String, /// optional address and key requesting to view the token owner @@ -684,8 +708,10 @@ pub enum Snip721QueryMsg { include_expired: Option, }, /// displays the public metadata of a token + #[returns(QueryAnswer)] NftInfo { token_id: String }, /// displays all the information contained in the OwnerOf and NftInfo queries + #[returns(QueryAnswer)] AllNftInfo { token_id: String, /// optional address and key requesting to view the token owner @@ -695,6 +721,7 @@ pub enum Snip721QueryMsg { include_expired: Option, }, /// displays the private metadata if permitted to view it + #[returns(QueryAnswer)] PrivateMetadata { token_id: String, /// optional address and key requesting to view the private metadata @@ -704,6 +731,7 @@ pub enum Snip721QueryMsg { /// see. This may include the owner, the public metadata, the private metadata, royalty /// information, mint run information, whether the token is unwrapped, whether the token is /// transferable, and the token and inventory approvals + #[returns(QueryAnswer)] NftDossier { token_id: String, /// optional address and key requesting to view the token information @@ -716,6 +744,7 @@ pub enum Snip721QueryMsg { /// see. This may include the owner, the public metadata, the private metadata, royalty /// information, mint run information, whether the token is unwrapped, whether the token is /// transferable, and the token and inventory approvals + #[returns(QueryAnswer)] BatchNftDossier { token_ids: Vec, /// optional address and key requesting to view the token information @@ -726,6 +755,7 @@ pub enum Snip721QueryMsg { }, /// list all the approvals in place for a specified token if given the owner's viewing /// key + #[returns(QueryAnswer)] TokenApprovals { token_id: String, /// the token owner's viewing key @@ -736,6 +766,7 @@ pub enum Snip721QueryMsg { }, /// list all the inventory-wide approvals in place for the specified address if given the /// the correct viewing key for the address + #[returns(QueryAnswer)] InventoryApprovals { address: String, /// the viewing key @@ -748,6 +779,7 @@ pub enum Snip721QueryMsg { /// approval to transfer all of the owner's tokens). This query is provided to maintain /// CW721 compliance, however, approvals are private on secret network, so only the /// owner's viewing key will authorize the ability to see the list of operators + #[returns(QueryAnswer)] ApprovedForAll { owner: String, /// optional viewing key to authenticate this query. It is "optional" only in the @@ -760,6 +792,7 @@ pub enum Snip721QueryMsg { }, /// displays a list of all the tokens belonging to the input owner in which the viewer /// has view_owner permission + #[returns(QueryAnswer)] Tokens { owner: String, /// optional address of the querier if different from the owner @@ -773,6 +806,7 @@ pub enum Snip721QueryMsg { }, /// displays the number of tokens that the querier has permission to see the owner and that /// belong to the specified address + #[returns(QueryAnswer)] NumTokensOfOwner { owner: String, /// optional address of the querier if different from the owner @@ -781,15 +815,20 @@ pub enum Snip721QueryMsg { viewing_key: Option, }, /// display if a token is unwrapped + #[returns(QueryAnswer)] IsUnwrapped { token_id: String }, /// display if a token is transferable + #[returns(QueryAnswer)] IsTransferable { token_id: String }, /// display that this contract implements non-transferable tokens + #[returns(QueryAnswer)] ImplementsNonTransferableTokens {}, /// display that this contract implements the use of the `token_subtype` metadata extension field + #[returns(QueryAnswer)] ImplementsTokenSubtype {}, /// verify that the specified address has approval to transfer every listed token. /// A token will count as unapproved if it is non-transferable + #[returns(QueryAnswer)] VerifyTransferApproval { /// list of tokens to verify approval for token_ids: Vec, @@ -800,6 +839,7 @@ pub enum Snip721QueryMsg { }, /// display the transaction history for the specified address in reverse /// chronological order + #[returns(QueryAnswer)] TransactionHistory { address: String, /// viewing key @@ -811,12 +851,14 @@ pub enum Snip721QueryMsg { }, /// display the code hash a contract has registered with the token contract and whether /// the contract implements BatchReceivenft + #[returns(QueryAnswer)] RegisteredCodeHash { /// the contract whose receive registration info you want to view contract: String, }, /// display the royalty information of a token if a token ID is specified, or display the /// contract's default royalty information in no token ID is provided + #[returns(QueryAnswer)] RoyaltyInfo { /// optional ID of the token whose royalty information should be displayed. If not /// provided, display the contract's default royalty information @@ -825,14 +867,20 @@ pub enum Snip721QueryMsg { viewer: Option, }, /// display the contract's creator + #[returns(QueryAnswer)] ContractCreator {}, /// perform queries by passing permits instead of viewing keys + #[returns(QueryAnswer)] WithPermit { /// permit used to verify querier identity permit: Permit, /// query to perform query: QueryWithPermit, }, + /// Extension for queries. The default implementation will do + /// nothing if queried for will return `Binary::default()`. + #[returns(QueryAnswer)] + QueryExtension { msg: QueryExt }, } /// SNIP721 Approval @@ -888,9 +936,33 @@ pub struct BatchNftDossierElement { pub inventory_approvals: Option>, } +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +pub struct Minters { + pub minters: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +pub struct NftInfo { + pub token_uri: Option, + pub extension: Option, + pub metadata_extension: MetadataExt, +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +pub struct OwnerOf { + pub owner: Addr, + pub approvals: Vec, +} + +#[derive(Serialize, Deserialize, JsonSchema, Debug)] +pub struct ContractInfo { + pub name: String, + pub symbol: String, +} + #[derive(Serialize, Deserialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] -pub enum Snip721QueryAnswer { +pub enum QueryAnswer { ContractInfo { name: String, symbol: String, @@ -998,6 +1070,9 @@ pub enum Snip721QueryAnswer { ContractCreator { creator: Option, }, + QueryExtension { + res: Binary, + }, } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] @@ -1141,345 +1216,3 @@ pub enum QueryWithPermit { /// belong to the specified address NumTokensOfOwner { owner: String }, } - -/// information about the minting of the NFT -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct MintRunInfo { - /// optional address of the SNIP-721 contract creator - pub collection_creator: Option, - /// optional address of this NFT's creator - pub token_creator: Option, - /// optional time of minting (in seconds since 01/01/1970) - pub time_of_minting: Option, - /// optional number of the mint run this token was minted in. A mint run represents a - /// batch of NFTs released at the same time. So if a creator decided to make 100 copies - /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and - /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all - /// be part of mint run number 2. - pub mint_run: Option, - /// optional serial number in this mint run. This is used to serialize - /// identical NFTs - pub serial_number: Option, - /// optional total number of NFTs minted on this run. This is used to - /// represent that this token is number m of n - pub quantity_minted_this_run: Option, -} - -/// Serial number to give an NFT when minting -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct SerialNumber { - /// optional number of the mint run this token will be minted in. A mint run represents a - /// batch of NFTs released at the same time. So if a creator decided to make 100 copies - /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and - /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all - /// be part of mint run number 2. - pub mint_run: Option, - /// serial number (in this mint run). This is used to serialize - /// identical NFTs - pub serial_number: u32, - /// optional total number of NFTs minted on this run. This is used to - /// represent that this token is number m of n - pub quantity_minted_this_run: Option, -} - -#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -/// at the given point in time and after, Expiration will be considered expired -pub enum Expiration { - /// expires at this block height - AtHeight(u64), - /// expires at the time in seconds since 01/01/1970 - AtTime(u64), - /// never expires - Never, -} - -impl fmt::Display for Expiration { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Expiration::AtHeight(height) => write!(f, "expiration height: {}", height), - Expiration::AtTime(time) => write!(f, "expiration time: {}", time), - Expiration::Never => write!(f, "expiration: never"), - } - } -} - -/// default is Never -impl Default for Expiration { - fn default() -> Self { - Expiration::Never - } -} - -impl Expiration { - /// Returns bool, true if Expiration has expired - /// - /// # Arguments - /// - /// * `block` - a reference to the BlockInfo containing the time to compare the Expiration to - pub fn is_expired(&self, block: &BlockInfo) -> bool { - match self { - Expiration::AtHeight(height) => block.height >= *height, - Expiration::AtTime(time) => block.time.seconds() >= *time, - Expiration::Never => false, - } - } -} - -/// data for a single royalty -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct Royalty { - /// address to send royalties to - pub recipient: String, - /// royalty rate - pub rate: u16, -} - -impl Royalty { - /// Returns StdResult from creating a StoredRoyalty from a Royalty - /// - /// # Arguments - /// - /// * `api` - a reference to the Api used to convert human and canonical addresses - pub fn to_stored(&self, api: &dyn Api) -> StdResult { - Ok(StoredRoyalty { - recipient: api.addr_canonicalize(api.addr_validate(&self.recipient)?.as_str())?, - rate: self.rate, - }) - } -} - -/// all royalty information -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct RoyaltyInfo { - /// decimal places in royalty rates - pub decimal_places_in_rates: u8, - /// list of royalties - pub royalties: Vec, -} - -impl RoyaltyInfo { - /// Returns StdResult from creating a StoredRoyaltyInfo from a RoyaltyInfo - /// - /// # Arguments - /// - /// * `api` - a reference to the Api used to convert human and canonical addresses - pub fn to_stored(&self, api: &dyn Api) -> StdResult { - Ok(StoredRoyaltyInfo { - decimal_places_in_rates: self.decimal_places_in_rates, - royalties: self - .royalties - .iter() - .map(|r| r.to_stored(api)) - .collect::>>()?, - }) - } -} - -/// display for a single royalty -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct DisplayRoyalty { - /// address to send royalties to. Can be None to keep addresses private - pub recipient: Option, - /// royalty rate - pub rate: u16, -} - -/// display all royalty information -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct DisplayRoyaltyInfo { - /// decimal places in royalty rates - pub decimal_places_in_rates: u8, - /// list of royalties - pub royalties: Vec, -} - -/// data for storing a single royalty -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct StoredRoyalty { - /// address to send royalties to - pub recipient: CanonicalAddr, - /// royalty rate - pub rate: u16, -} - -impl StoredRoyalty { - /// Returns StdResult from creating a DisplayRoyalty from a StoredRoyalty - /// - /// # Arguments - /// - /// * `api` - a reference to the Api used to convert human and canonical addresses - /// * `hide_addr` - true if the address should be kept hidden - pub fn to_human(&self, api: &dyn Api, hide_addr: bool) -> StdResult { - let recipient = if hide_addr { - None - } else { - Some(api.addr_humanize(&self.recipient)?) - }; - Ok(DisplayRoyalty { - recipient, - rate: self.rate, - }) - } -} - -/// all stored royalty information -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -pub struct StoredRoyaltyInfo { - /// decimal places in royalty rates - pub decimal_places_in_rates: u8, - /// list of royalties - pub royalties: Vec, -} - -impl StoredRoyaltyInfo { - /// Returns StdResult from creating a DisplayRoyaltyInfo from a StoredRoyaltyInfo - /// - /// # Arguments - /// - /// * `api` - a reference to the Api used to convert human and canonical addresses - /// * `hide_addr` - true if the address should be kept hidden - pub fn to_human(&self, api: &dyn Api, hide_addr: bool) -> StdResult { - Ok(DisplayRoyaltyInfo { - decimal_places_in_rates: self.decimal_places_in_rates, - royalties: self - .royalties - .iter() - .map(|r| r.to_human(api, hide_addr)) - .collect::>>()?, - }) - } -} - -/// token -#[derive(Serialize, Deserialize)] -pub struct Token { - /// owner - pub owner: CanonicalAddr, - /// permissions granted for this token - pub permissions: Vec, - /// true if this token has been unwrapped. If sealed metadata is not enabled, all - /// tokens are considered unwrapped - pub unwrapped: bool, - /// true if this token is transferable - pub transferable: bool, -} - -/// token metadata -#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] -pub struct Metadata { - /// optional uri for off-chain metadata. This should be prefixed with `http://`, `https://`, `ipfs://`, or - /// `ar://`. Only use this if you are not using `extension` - pub token_uri: Option, - /// optional on-chain metadata. Only use this if you are not using `token_uri` - pub extension: Option, -} - -/// metadata extension -/// You can add any metadata fields you need here. These fields are based on -/// https://docs.opensea.io/docs/metadata-standards and are the metadata fields that -/// Stashh uses for robust NFT display. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or -/// `ar://` -#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] -pub struct Extension { - /// url to the image - pub image: Option, - /// raw SVG image data (not recommended). Only use this if you're not including the image parameter - pub image_data: Option, - /// url to allow users to view the item on your site - pub external_url: Option, - /// item description - pub description: Option, - /// name of the item - pub name: Option, - /// item attributes - pub attributes: Option>, - /// background color represented as a six-character hexadecimal without a pre-pended # - pub background_color: Option, - /// url to a multimedia attachment - pub animation_url: Option, - /// url to a YouTube video - pub youtube_url: Option, - /// media files as specified on Stashh that allows for basic authenticatiion and decryption keys. - /// Most of the above is used for bridging public eth NFT metadata easily, whereas `media` will be used - /// when minting NFTs on Stashh - pub media: Option>, - /// a select list of trait_types that are in the private metadata. This will only ever be used - /// in public metadata - pub protected_attributes: Option>, - /// token subtypes used by Stashh for display groupings (primarily used for badges, which are specified - /// by using "badge" as the token_subtype) - pub token_subtype: Option, - - /// Optional on-chain role for this member, can be used by other contracts to enforce permissions - pub role: Option, - /// The voting weight of this role - pub weight: u64, -} - -/// attribute trait -#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] -pub struct Trait { - /// indicates how a trait should be displayed - pub display_type: Option, - /// name of the trait - pub trait_type: Option, - /// trait value - pub value: String, - /// optional max value for numerical traits - pub max_value: Option, -} - -/// media file -#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] -pub struct MediaFile { - /// file type - /// Stashh currently uses: "image", "video", "audio", "text", "font", "application" - pub file_type: Option, - /// file extension - pub extension: Option, - /// authentication information - pub authentication: Option, - /// url to the file. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://` - pub url: String, -} - -/// media file authentication -#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] -pub struct Authentication { - /// either a decryption key for encrypted files or a password for basic authentication - pub key: Option, - /// username used in basic authentication - pub user: Option, -} - -/// permission to view token info/transfer tokens -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -pub struct Permission { - /// permitted address - pub address: CanonicalAddr, - /// list of permission expirations for this address - pub expirations: [Option; 3], -} - -impl InitCallback for Snip721InstantiateMsg { - const BLOCK_SIZE: usize = 256; -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct NftInfo { - pub token_uri: Option, - pub extension: Option, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug)] -pub struct OwnerOf { - pub owner: Addr, - pub approvals: Vec, -} - -#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] -pub struct InstantiateResponse { - pub contract_address: Addr, - pub code_hash: String, -} diff --git a/contracts/external/snip721-roles-impl/src/receiver.rs b/contracts/external/snip721-roles-impl/src/receiver.rs new file mode 100644 index 0000000..299051f --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/receiver.rs @@ -0,0 +1,107 @@ +use cosmwasm_std::{Addr, Binary, CosmosMsg, StdResult}; +use schemars::JsonSchema; +use secret_toolkit::utils::HandleCallback; +use serde::{Deserialize, Serialize}; + +use crate::contract::BLOCK_SIZE; + +/// used to create ReceiveNft and BatchReceiveNft callback messages. BatchReceiveNft is preferred +/// over ReceiveNft, because ReceiveNft does not allow the recipient to know who sent the token, +/// only its previous owner, and ReceiveNft can only process one token. So it is inefficient when +/// sending multiple tokens to the same contract (a deck of game cards for instance). ReceiveNft +/// primarily exists just to maintain CW-721 compliance. Also, it should be noted that the CW-721 +/// `sender` field is inaccurately named, because it is used to hold the address the token came from, +/// not the address that sent it (which is not always the same). The name is reluctantly kept in +/// ReceiveNft to maintain CW-721 compliance, but BatchReceiveNft uses `sender` to hold the sending +/// address (which matches both its true role and its SNIP-20 Receive counterpart). Any contract +/// that is implementing both Receiver Interfaces must be sure that the ReceiveNft `sender` field +/// is actually processed like a BatchReceiveNft `from` field. Again, apologies for any confusion +/// caused by propagating inaccuracies, but because InterNFT is planning on using CW-721 standards, +/// compliance with CW-721 might be necessary +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Snip721RolesReceiveMsg { + /// ReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver + /// interface. BatchReceiveNft, which is more informative and more efficient, is preferred over + /// ReceiveNft. Please read above regarding why ReceiveNft, which follows CW-721 standard has an + /// inaccurately named `sender` field + ReceiveNft { + /// previous owner of sent token + sender: Addr, + /// token that was sent + token_id: String, + /// optional message to control receiving logic + msg: Option, + }, + /// BatchReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver + /// interface. BatchReceiveNft, which is more informative and more efficient, is preferred over + /// ReceiveNft. + BatchReceiveNft { + /// address that sent the tokens. There is no ReceiveNft field equivalent to this + sender: Addr, + /// previous owner of sent tokens. This is equivalent to the ReceiveNft `sender` field + from: Addr, + /// tokens that were sent + token_ids: Vec, + /// optional message to control receiving logic + msg: Option, + }, +} + +impl HandleCallback for Snip721RolesReceiveMsg { + const BLOCK_SIZE: usize = BLOCK_SIZE; +} + +/// Returns a StdResult used to call a registered contract's ReceiveNft +/// +/// # Arguments +/// +/// * `sender` - the address of the former owner of the sent token +/// * `token_id` - ID String of the token that was sent +/// * `msg` - optional msg used to control ReceiveNft logic +/// * `callback_code_hash` - String holding the code hash of the contract that was +/// sent the token +/// * `contract_addr` - address of the contract that was sent the token +pub fn receive_nft_msg( + sender: Addr, + token_id: String, + msg: Option, + callback_code_hash: String, + contract_addr: String, +) -> StdResult { + let msg = Snip721RolesReceiveMsg::ReceiveNft { + sender, + token_id, + msg, + }; + msg.to_cosmos_msg(callback_code_hash, contract_addr, None) +} + +/// Returns a StdResult used to call a registered contract's +/// BatchReceiveNft +/// +/// # Arguments +/// +/// * `sender` - the address that is sending the token +/// * `from` - the address of the former owner of the sent token +/// * `token_ids` - list of ID Strings of the tokens that were sent +/// * `msg` - optional msg used to control ReceiveNft logic +/// * `callback_code_hash` - String holding the code hash of the contract that was +/// sent the token +/// * `contract_addr` - address of the contract that was sent the token +pub fn batch_receive_nft_msg( + sender: Addr, + from: Addr, + token_ids: Vec, + msg: Option, + callback_code_hash: String, + contract_addr: String, +) -> StdResult { + let msg = Snip721RolesReceiveMsg::BatchReceiveNft { + sender, + from, + token_ids, + msg, + }; + msg.to_cosmos_msg(callback_code_hash, contract_addr, None) +} diff --git a/contracts/external/snip721-roles-impl/src/royalties.rs b/contracts/external/snip721-roles-impl/src/royalties.rs new file mode 100644 index 0000000..4924180 --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/royalties.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::{Addr, Api, CanonicalAddr, StdResult}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// data for a single royalty +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct Royalty { + /// address to send royalties to + pub recipient: String, + /// royalty rate + pub rate: u16, +} + +impl Royalty { + /// Returns StdResult from creating a StoredRoyalty from a Royalty + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + pub fn to_stored(&self, api: &dyn Api) -> StdResult { + Ok(StoredRoyalty { + recipient: api.addr_canonicalize(api.addr_validate(&self.recipient)?.as_str())?, + rate: self.rate, + }) + } +} + +/// all royalty information +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct RoyaltyInfo { + /// decimal places in royalty rates + pub decimal_places_in_rates: u8, + /// list of royalties + pub royalties: Vec, +} + +impl RoyaltyInfo { + /// Returns StdResult from creating a StoredRoyaltyInfo from a RoyaltyInfo + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + pub fn to_stored(&self, api: &dyn Api) -> StdResult { + Ok(StoredRoyaltyInfo { + decimal_places_in_rates: self.decimal_places_in_rates, + royalties: self + .royalties + .iter() + .map(|r| r.to_stored(api)) + .collect::>>()?, + }) + } +} + +/// display for a single royalty +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct DisplayRoyalty { + /// address to send royalties to. Can be None to keep addresses private + pub recipient: Option, + /// royalty rate + pub rate: u16, +} + +/// display all royalty information +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct DisplayRoyaltyInfo { + /// decimal places in royalty rates + pub decimal_places_in_rates: u8, + /// list of royalties + pub royalties: Vec, +} + +/// data for storing a single royalty +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct StoredRoyalty { + /// address to send royalties to + pub recipient: CanonicalAddr, + /// royalty rate + pub rate: u16, +} + +impl StoredRoyalty { + /// Returns StdResult from creating a DisplayRoyalty from a StoredRoyalty + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + /// * `hide_addr` - true if the address should be kept hidden + pub fn to_human(&self, api: &dyn Api, hide_addr: bool) -> StdResult { + let recipient = if hide_addr { + None + } else { + Some(api.addr_humanize(&self.recipient)?) + }; + Ok(DisplayRoyalty { + recipient, + rate: self.rate, + }) + } +} + +/// all stored royalty information +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct StoredRoyaltyInfo { + /// decimal places in royalty rates + pub decimal_places_in_rates: u8, + /// list of royalties + pub royalties: Vec, +} + +impl StoredRoyaltyInfo { + /// Returns StdResult from creating a DisplayRoyaltyInfo from a StoredRoyaltyInfo + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + /// * `hide_addr` - true if the address should be kept hidden + pub fn to_human(&self, api: &dyn Api, hide_addr: bool) -> StdResult { + Ok(DisplayRoyaltyInfo { + decimal_places_in_rates: self.decimal_places_in_rates, + royalties: self + .royalties + .iter() + .map(|r| r.to_human(api, hide_addr)) + .collect::>>()?, + }) + } +} diff --git a/contracts/external/snip721-roles-impl/src/state.rs b/contracts/external/snip721-roles-impl/src/state.rs new file mode 100644 index 0000000..80f22af --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/state.rs @@ -0,0 +1,603 @@ +use std::{any::type_name, marker::PhantomData}; + +use cosmwasm_std::{Api, BlockInfo, CanonicalAddr, StdError, StdResult, Storage}; +use cosmwasm_storage::{PrefixedStorage, ReadonlyPrefixedStorage}; +use secret_toolkit::{ + serialization::{Bincode2, Json, Serde}, + storage::{AppendStore, Item, Keymap}, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use shade_protocol::Contract; + +use crate::msg::{Tx, TxAction}; +use crate::{expiration::Expiration, token::Metadata}; + +/// storage key for config +pub const CONFIG_KEY: &[u8] = b"config"; +/// storage key for minters +pub const MINTERS_KEY: &[u8] = b"minters"; +/// storage key for the contract instantiator +pub const CREATOR_KEY: &[u8] = b"creator"; +/// storage key for the default RoyaltyInfo to use if none is supplied when minting +pub const DEFAULT_ROYALTY_KEY: &[u8] = b"defaultroy"; +/// prefix for storage that maps ids to indices +pub const PREFIX_MAP_TO_INDEX: &[u8] = b"map2idx"; +/// prefix for storage that maps indices to ids +pub const PREFIX_MAP_TO_ID: &[u8] = b"idx2id"; +/// prefix for storage of token infos +pub const PREFIX_INFOS: &[u8] = b"infos"; +/// prefix for the storage of public metadata +pub const PREFIX_PUB_META: &[u8] = b"publicmeta"; +/// prefix for the storage of private metadata +pub const PREFIX_PRIV_META: &[u8] = b"privatemeta"; +/// prefix for the storage of royalty information +pub const PREFIX_ROYALTY_INFO: &[u8] = b"royalty"; +/// prefix for the storage of mint run information +pub const PREFIX_MINT_RUN: &[u8] = b"mintrun"; +/// prefix for storage of txs +pub const PREFIX_TXS: &[u8] = b"rawtxs"; +/// prefix for storage of owner's list of "all" permissions +pub const PREFIX_ALL_PERMISSIONS: &[u8] = b"allpermissions"; +/// prefix for storage of owner's list of tokens permitted to addresses +pub const PREFIX_AUTHLIST: &[u8] = b"authlist"; +/// prefix for storage of an address' ownership prvicacy +pub const PREFIX_OWNER_PRIV: &[u8] = b"ownerpriv"; +/// prefix for the storage of the code hashes of contract's that have implemented ReceiveNft +pub const PREFIX_RECEIVERS: &[u8] = b"receivers"; +/// prefix for the storage of mint run numbers +pub const PREFIX_MINT_RUN_NUM: &[u8] = b"runnum"; +/// prefix for the storage of revoked permits +pub const PREFIX_REVOKED_PERMITS: &str = "revoke"; + +/// viewing key error message +pub const VIEWING_KEY_ERR_MSG: &str = "Wrong viewing key for this address or viewing key not set"; + +// append store for user's list of tx ids +pub static TX_ID_STORE: AppendStore = AppendStore::new(b"txid"); + +/// Token contract config +#[derive(Serialize, Debug, Deserialize, Clone, PartialEq, Eq)] +pub struct Config { + /// name of token contract + pub name: String, + /// token contract symbol + pub symbol: String, + /// admin address + pub admin: CanonicalAddr, + /// count of mint ops + pub mint_cnt: u32, + /// count of tx + pub tx_cnt: u64, + /// token count + pub token_cnt: u32, + /// contract status + pub status: u8, + /// are token IDs/count public + pub token_supply_is_public: bool, + /// is ownership public + pub owner_is_public: bool, + /// is sealed metadata enabled + pub sealed_metadata_is_enabled: bool, + /// should Reveal unwrap to private metadata + pub unwrap_to_private: bool, + /// is a minter permitted to update a token's metadata + pub minter_may_update_metadata: bool, + /// is the token's owner permitted to update the token's metadata + pub owner_may_update_metadata: bool, + /// is burn enabled + pub burn_is_enabled: bool, +} + +/// tx type and specifics +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum StoredTxAction { + /// transferred token ownership + Transfer { + /// previous owner + from: CanonicalAddr, + /// optional sender if not owner + sender: Option, + /// new owner + recipient: CanonicalAddr, + }, + /// minted new token + Mint { + /// minter's address + minter: CanonicalAddr, + /// token's first owner + recipient: CanonicalAddr, + }, + /// burned a token + Burn { + /// previous owner + owner: CanonicalAddr, + /// burner's address if not owner + burner: Option, + }, +} + +/// tx in storage +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub struct StoredTx { + /// tx id + pub tx_id: u64, + /// the block containing this tx + pub block_height: u64, + /// the time (in seconds since 01/01/1970) of the block containing this tx + pub block_time: u64, + /// token id + pub token_id: String, + /// tx type and specifics + pub action: StoredTxAction, + /// optional memo + pub memo: Option, +} + +impl StoredTx { + /// Returns StdResult from converting a stored tx to a displayable tx + /// + /// # Arguments + /// + /// * `api` - a reference to the Api used to convert human and canonical addresses + pub fn into_humanized(self, api: &dyn Api) -> StdResult { + let action = match self.action { + StoredTxAction::Transfer { + from, + sender, + recipient, + } => { + let sndr = if let Some(s) = sender { + Some(api.addr_humanize(&s)?) + } else { + None + }; + TxAction::Transfer { + from: api.addr_humanize(&from)?, + sender: sndr, + recipient: api.addr_humanize(&recipient)?, + } + } + StoredTxAction::Mint { minter, recipient } => TxAction::Mint { + minter: api.addr_humanize(&minter)?, + recipient: api.addr_humanize(&recipient)?, + }, + StoredTxAction::Burn { owner, burner } => { + let bnr = if let Some(b) = burner { + Some(api.addr_humanize(&b)?) + } else { + None + }; + TxAction::Burn { + owner: api.addr_humanize(&owner)?, + burner: bnr, + } + } + }; + let tx = Tx { + tx_id: self.tx_id, + block_height: self.block_height, + block_time: self.block_time, + token_id: self.token_id, + action, + memo: self.memo, + }; + + Ok(tx) + } +} + +/// Returns StdResult<()> after storing tx +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `config` - a mutable reference to the contract Config +/// * `block` - a reference to the current BlockInfo +/// * `token_id` - token id being minted +/// * `from` - the previouis owner's address +/// * `sender` - optional address that sent the token +/// * `recipient` - the recipient's address +/// * `memo` - optional memo for the tx +#[allow(clippy::too_many_arguments)] +pub fn store_transfer( + storage: &mut dyn Storage, + config: &mut Config, + block: &BlockInfo, + token_id: String, + from: CanonicalAddr, + sender: Option, + recipient: CanonicalAddr, + memo: Option, +) -> StdResult<()> { + let action = StoredTxAction::Transfer { + from, + sender, + recipient, + }; + let tx = StoredTx { + tx_id: config.tx_cnt, + block_height: block.height, + block_time: block.time.seconds(), + token_id, + action, + memo, + }; + let mut tx_store = PrefixedStorage::new(storage, PREFIX_TXS); + json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?; + if let StoredTxAction::Transfer { + from, + sender, + recipient, + } = tx.action + { + append_tx_for_addr(storage, config.tx_cnt, &from)?; + append_tx_for_addr(storage, config.tx_cnt, &recipient)?; + if let Some(sndr) = sender.as_ref() { + if *sndr != recipient { + append_tx_for_addr(storage, config.tx_cnt, sndr)?; + } + } + } + config.tx_cnt += 1; + Ok(()) +} + +/// Returns StdResult<()> after storing tx +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `config` - a mutable reference to the contract Config +/// * `block` - a reference to the current BlockInfo +/// * `token_id` - token id being minted +/// * `minter` - the minter's address +/// * `recipient` - the recipient's address +/// * `memo` - optional memo for the tx +pub fn store_mint( + storage: &mut dyn Storage, + config: &mut Config, + block: &BlockInfo, + token_id: String, + minter: CanonicalAddr, + recipient: CanonicalAddr, + memo: Option, +) -> StdResult<()> { + let action = StoredTxAction::Mint { minter, recipient }; + let tx = StoredTx { + tx_id: config.tx_cnt, + block_height: block.height, + block_time: block.time.seconds(), + token_id, + action, + memo, + }; + let mut tx_store = PrefixedStorage::new(storage, PREFIX_TXS); + json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?; + if let StoredTxAction::Mint { minter, recipient } = tx.action { + append_tx_for_addr(storage, config.tx_cnt, &recipient)?; + if recipient != minter { + append_tx_for_addr(storage, config.tx_cnt, &minter)?; + } + } + config.tx_cnt += 1; + Ok(()) +} + +/// Returns StdResult<()> after storing tx +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `config` - a mutable reference to the contract Config +/// * `block` - a reference to the current BlockInfo +/// * `token_id` - token id being minted +/// * `owner` - the previous owner's address +/// * `burner` - optional address that burnt the token +/// * `memo` - optional memo for the tx +pub fn store_burn( + storage: &mut dyn Storage, + config: &mut Config, + block: &BlockInfo, + token_id: String, + owner: CanonicalAddr, + burner: Option, + memo: Option, +) -> StdResult<()> { + let action = StoredTxAction::Burn { owner, burner }; + let tx = StoredTx { + tx_id: config.tx_cnt, + block_height: block.height, + block_time: block.time.seconds(), + token_id, + action, + memo, + }; + let mut tx_store = PrefixedStorage::new(storage, PREFIX_TXS); + json_save(&mut tx_store, &config.tx_cnt.to_le_bytes(), &tx)?; + if let StoredTxAction::Burn { owner, burner } = tx.action { + append_tx_for_addr(storage, config.tx_cnt, &owner)?; + if let Some(bnr) = burner.as_ref() { + append_tx_for_addr(storage, config.tx_cnt, bnr)?; + } + } + config.tx_cnt += 1; + Ok(()) +} + +/// Returns StdResult<()> after saving tx id +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `tx_id` - the tx id to store +/// * `address` - a reference to the address for which to store this tx id +fn append_tx_for_addr( + storage: &mut dyn Storage, + tx_id: u64, + address: &CanonicalAddr, +) -> StdResult<()> { + let addr_store = TX_ID_STORE.add_suffix(address.as_slice()); + addr_store.push(storage, &tx_id) +} + +/// Returns StdResult<(Vec, u64)> of the txs to display and the total count of txs +/// +/// # Arguments +/// +/// * `api` - a reference to the Api used to convert human and canonical addresses +/// * `storage` - a reference to the contract's storage +/// * `address` - a reference to the address whose txs to display +/// * `page` - page to start displaying +/// * `page_size` - number of txs per page +pub fn get_txs( + api: &dyn Api, + storage: &dyn Storage, + address: &CanonicalAddr, + page: u32, + page_size: u32, +) -> StdResult<(Vec, u64)> { + let addr_store = TX_ID_STORE.add_suffix(address.as_slice()); + + let count = addr_store.get_len(storage)? as u64; + // access tx storage + let tx_store = ReadonlyPrefixedStorage::new(storage, PREFIX_TXS); + // Take `page_size` txs starting from the latest tx, potentially skipping `page * page_size` + // txs from the start. + let txs: StdResult> = addr_store + .iter(storage)? + .rev() + .skip((page * page_size) as usize) + .take(page_size as usize) + .map(|id| { + id.and_then(|id| { + json_load(&tx_store, &id.to_le_bytes()) + .and_then(|tx: StoredTx| tx.into_humanized(api)) + }) + }) + .collect(); + + txs.map(|t| (t, count)) +} + +/// permission to view token info/transfer tokens +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] +pub struct Permission { + /// permitted address + pub address: CanonicalAddr, + /// list of permission expirations for this address + pub expirations: [Option; 3], +} + +/// permission types +#[derive(Serialize, Deserialize, Debug)] +pub enum PermissionType { + ViewOwner, + ViewMetadata, + Transfer, +} + +impl PermissionType { + /// Returns usize representation of the enum variant + pub fn to_usize(&self) -> usize { + match self { + PermissionType::ViewOwner => 0, + PermissionType::ViewMetadata => 1, + PermissionType::Transfer => 2, + } + } + + /// returns the number of permission types + pub fn num_types(&self) -> usize { + 3 + } +} + +/// list of one owner's tokens authorized to a single address +#[derive(Serialize, Deserialize, Debug)] +pub struct AuthList { + /// whitelisted address + pub address: CanonicalAddr, + /// lists of tokens address has access to + pub tokens: [Vec; 3], +} + +/// a contract's code hash and whether they implement BatchReceiveNft +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ReceiveRegistration { + /// code hash of the contract + pub code_hash: String, + /// true if the contract implements BatchReceiveNft + pub impl_batch: bool, +} + +/// Returns StdResult<()> resulting from saving an item to storage +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `key` - a byte slice representing the key to access the stored item +/// * `value` - a reference to the item to store +pub fn save(storage: &mut dyn Storage, key: &[u8], value: &T) -> StdResult<()> { + storage.set(key, &Bincode2::serialize(value)?); + Ok(()) +} + +/// Removes an item from storage +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item is in +/// * `key` - a byte slice representing the key that accesses the stored item +pub fn remove(storage: &mut dyn Storage, key: &[u8]) { + storage.remove(key); +} + +/// Returns StdResult from retrieving the item with the specified key. Returns a +/// StdError::NotFound if there is no item with that key +/// +/// # Arguments +/// +/// * `storage` - a reference to the storage this item is in +/// * `key` - a byte slice representing the key that accesses the stored item +pub fn load(storage: &dyn Storage, key: &[u8]) -> StdResult { + Bincode2::deserialize( + &storage + .get(key) + .ok_or_else(|| StdError::not_found(type_name::()))?, + ) +} + +/// Returns StdResult> from retrieving the item with the specified key. +/// Returns Ok(None) if there is no item with that key +/// +/// # Arguments +/// +/// * `storage` - a reference to the storage this item is in +/// * `key` - a byte slice representing the key that accesses the stored item +pub fn may_load(storage: &dyn Storage, key: &[u8]) -> StdResult> { + match storage.get(key) { + Some(value) => Bincode2::deserialize(&value).map(Some), + None => Ok(None), + } +} + +/// Returns StdResult<()> resulting from saving an item to storage using Json (de)serialization +/// because bincode2 annoyingly uses a float op when deserializing an enum +/// +/// # Arguments +/// +/// * `storage` - a mutable reference to the storage this item should go to +/// * `key` - a byte slice representing the key to access the stored item +/// * `value` - a reference to the item to store +pub fn json_save(storage: &mut dyn Storage, key: &[u8], value: &T) -> StdResult<()> { + storage.set(key, &Json::serialize(value)?); + Ok(()) +} + +/// Returns StdResult from retrieving the item with the specified key using Json +/// (de)serialization because bincode2 annoyingly uses a float op when deserializing an enum. +/// Returns a StdError::NotFound if there is no item with that key +/// +/// # Arguments +/// +/// * `storage` - a reference to the storage this item is in +/// * `key` - a byte slice representing the key that accesses the stored item +pub fn json_load(storage: &dyn Storage, key: &[u8]) -> StdResult { + Json::deserialize( + &storage + .get(key) + .ok_or_else(|| StdError::not_found(type_name::()))?, + ) +} + +/// Returns StdResult> from retrieving the item with the specified key using Json +/// (de)serialization because bincode2 annoyingly uses a float op when deserializing an enum. +/// Returns Ok(None) if there is no item with that key +/// +/// # Arguments +/// +/// * `storage` - a reference to the storage this item is in +/// * `key` - a byte slice representing the key that accesses the stored item +pub fn json_may_load( + storage: &dyn Storage, + key: &[u8], +) -> StdResult> { + match storage.get(key) { + Some(value) => Json::deserialize(&value).map(Some), + None => Ok(None), + } +} + +pub struct Snip721Contract +where + MetadataExt: DeserializeOwned + Serialize, +{ + // /// The proposal module that this module is associated with. + // pub proposal_module: Item<'static, AnyContractInfo>, + // /// The DAO (dao-dao-core module) that this module is associated + // /// with. + // pub dao: Item<'static, AnyContractInfo>, + // /// The configuration for this module. + // pub config: Item<'static, Config>, + // /// Map between proposal IDs and (deposit, proposer) pairs. + // pub deposits: Keymap<'static, u64, (Option, Addr), Json>, + // /// Consumers of proposal submitted hooks. + // pub proposal_submitted_hooks: Hooks<'static>, + + // These types are used in associated functions, but not + // assocaited data. To stop the compiler complaining about unused + // generics, we build this phantom data. + instantiate_type: PhantomData, + execute_type: PhantomData, + query_type: PhantomData, + metadata_type: PhantomData, + + pub token_extension_info: Keymap<'static, String, MetadataExt, Json>, + pub pub_metadata: Keymap<'static, String, Metadata, Json>, + pub priv_metadata: Keymap<'static, String, Metadata, Json>, + pub query_auth: Item<'static, Contract>, +} + +impl + Snip721Contract +where + MetadataExt: DeserializeOwned + Serialize, +{ + const fn new( + token_extension_key: &'static str, + pub_metadata_key: &'static str, + priv_metadata_key: &'static str, + query_auth_key: &'static str, + ) -> Self { + Self { + execute_type: PhantomData, + instantiate_type: PhantomData, + query_type: PhantomData, + metadata_type: PhantomData, + token_extension_info: Keymap::new(token_extension_key.as_bytes()), + pub_metadata: Keymap::new(pub_metadata_key.as_bytes()), + priv_metadata: Keymap::new(priv_metadata_key.as_bytes()), + query_auth: Item::new(query_auth_key.as_bytes()), + } + } +} + +impl Default + for Snip721Contract +where + MetadataExt: DeserializeOwned + Serialize, +{ + fn default() -> Self { + // Call into constant function here. Presumably, the compiler + // is clever enough to inline this. This gives us + // "more-or-less" constant evaluation for our default method. + Self::new( + "token_extension_info", + "pub_metadata", + "priv_metadata", + "query_auth", + ) + } +} diff --git a/contracts/external/snip721-roles-impl/src/token.rs b/contracts/external/snip721-roles-impl/src/token.rs new file mode 100644 index 0000000..35226fd --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/token.rs @@ -0,0 +1,102 @@ +use cosmwasm_std::CanonicalAddr; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::state::Permission; + +/// token +#[derive(Serialize, Deserialize)] +pub struct Token { + /// owner + pub owner: CanonicalAddr, + /// permissions granted for this token + pub permissions: Vec, + /// true if this token has been unwrapped. If sealed metadata is not enabled, all + /// tokens are considered unwrapped + pub unwrapped: bool, + /// true if this token is transferable + pub transferable: bool, +} + +/// token metadata +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Metadata { + /// optional uri for off-chain metadata. This should be prefixed with `http://`, `https://`, `ipfs://`, or + /// `ar://`. Only use this if you are not using `extension` + pub token_uri: Option, + /// optional on-chain metadata. Only use this if you are not using `token_uri` + pub extension: Option, +} + +/// metadata extension +/// You can add any metadata fields you need here. These fields are based on +/// https://docs.opensea.io/docs/metadata-standards and are the metadata fields that +/// Stashh uses for robust NFT display. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or +/// `ar://` +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Extension { + /// url to the image + pub image: Option, + /// raw SVG image data (not recommended). Only use this if you're not including the image parameter + pub image_data: Option, + /// url to allow users to view the item on your site + pub external_url: Option, + /// item description + pub description: Option, + /// name of the item + pub name: Option, + /// item attributes + pub attributes: Option>, + /// background color represented as a six-character hexadecimal without a pre-pended # + pub background_color: Option, + /// url to a multimedia attachment + pub animation_url: Option, + /// url to a YouTube video + pub youtube_url: Option, + /// media files as specified on Stashh that allows for basic authenticatiion and decryption keys. + /// Most of the above is used for bridging public eth NFT metadata easily, whereas `media` will be used + /// when minting NFTs on Stashh + pub media: Option>, + /// a select list of trait_types that are in the private metadata. This will only ever be used + /// in public metadata + pub protected_attributes: Option>, + /// token subtypes used by Stashh for display groupings (primarily used for badges, which are specified + /// by using "badge" as the token_subtype) + pub token_subtype: Option, +} + +/// attribute trait +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Trait { + /// indicates how a trait should be displayed + pub display_type: Option, + /// name of the trait + pub trait_type: Option, + /// trait value + pub value: String, + /// optional max value for numerical traits + pub max_value: Option, +} + +/// media file +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct MediaFile { + /// file type + /// Stashh currently uses: "image", "video", "audio", "text", "font", "application" + pub file_type: Option, + /// file extension + pub extension: Option, + /// authentication information + pub authentication: Option, + /// url to the file. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://` + pub url: String, +} + +/// media file authentication +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Authentication { + /// either a decryption key for encrypted files or a password for basic authentication + pub key: Option, + /// username used in basic authentication + pub user: Option, +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_handles.rs b/contracts/external/snip721-roles-impl/src/unittest_handles.rs new file mode 100644 index 0000000..4f140f8 --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_handles.rs @@ -0,0 +1,12800 @@ +#[cfg(test)] +mod tests { + use std::any::Any; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{ + from_binary, to_binary, Addr, Api, Binary, BlockInfo, CanonicalAddr, Coin, Env, OwnedDeps, + Response, StdError, StdResult, SubMsg, Timestamp, Uint128, WasmMsg, + }; + use cosmwasm_storage::ReadonlyPrefixedStorage; + use secret_toolkit::{ + utils::space_pad, + viewing_key::{ViewingKey, ViewingKeyStore}, + }; + + use crate::contract::{check_permission, execute, instantiate, query}; + use crate::expiration::Expiration; + use crate::inventory::Inventory; + use crate::msg::{ + AccessLevel, Burn, ContractStatus, ExecuteAnswer, ExecuteMsg, InstantiateConfig, + InstantiateMsg, Mint, PostInstantiateCallback, QueryAnswer, QueryMsg, ReceiverInfo, Send, + Transfer, Tx, TxAction, + }; + use crate::receiver::Snip721ReceiveMsg; + use crate::state::{ + get_txs, json_load, json_may_load, load, may_load, AuthList, Config, Permission, + PermissionType, CONFIG_KEY, MINTERS_KEY, PREFIX_ALL_PERMISSIONS, PREFIX_AUTHLIST, + PREFIX_INFOS, PREFIX_MAP_TO_ID, PREFIX_MAP_TO_INDEX, PREFIX_OWNER_PRIV, PREFIX_PRIV_META, + PREFIX_PUB_META, PREFIX_RECEIVERS, + }; + use crate::token::{Extension, Metadata, Token}; + + // Helper functions + + fn init_helper_default() -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn init_helper_with_config( + public_token_supply: bool, + public_owner: bool, + enable_sealed_metadata: bool, + unwrapped_metadata_is_private: bool, + minter_may_update_metadata: bool, + owner_may_update_metadata: bool, + enable_burn: bool, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + + let env = mock_env(); + let init_config: InstantiateConfig = from_binary(&Binary::from( + format!( + "{{\"public_token_supply\":{}, + \"public_owner\":{}, + \"enable_sealed_metadata\":{}, + \"unwrapped_metadata_is_private\":{}, + \"minter_may_update_metadata\":{}, + \"owner_may_update_metadata\":{}, + \"enable_burn\":{}}}", + public_token_supply, + public_owner, + enable_sealed_metadata, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + enable_burn, + ) + .as_bytes(), + )) + .unwrap(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: Some(init_config), + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(_response) => panic!("Expected error, but had Ok response"), + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected error result {:?}", err), + }, + } + } + + fn extract_log(resp: StdResult) -> String { + match resp { + Ok(response) => response.attributes[0].value.clone(), + Err(_err) => "These are not the logs you are looking for".to_string(), + } + } + + // Init tests + + #[test] + fn test_init_sanity() { + // test default + let (init_result, deps) = init_helper_default(); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(!config.token_supply_is_public); + assert!(!config.owner_is_public); + assert!(!config.sealed_metadata_is_enabled); + assert!(!config.unwrap_to_private); + assert!(config.minter_may_update_metadata); + assert!(!config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + // test config specification + let (init_result, deps) = + init_helper_with_config(true, true, true, true, false, true, false); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(config.token_supply_is_public); + assert!(config.owner_is_public); + assert!(config.sealed_metadata_is_enabled); + assert!(config.unwrap_to_private); + assert!(!config.minter_may_update_metadata); + assert!(config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + // test post init callback + let mut deps = mock_dependencies(); + let env = mock_env(); + // just picking a random short HandleMsg that wouldn't really make sense + let post_init_msg = to_binary(&ExecuteMsg::MakeOwnershipPrivate { padding: None }).unwrap(); + let post_init_send = vec![Coin { + amount: Uint128::new(100), + denom: "uscrt".to_string(), + }]; + let post_init_callback = Some(PostInstantiateCallback { + msg: post_init_msg.clone(), + contract_address: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + send: post_init_send.clone(), + }); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback, + }; + + let init_response = instantiate(deps.as_mut(), env, info, init_msg).unwrap(); + assert_eq!( + init_response.messages, + vec![SubMsg::new(WasmMsg::Execute { + msg: post_init_msg, + contract_addr: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + funds: post_init_send, + })] + ); + } + + // Handle tests + + // test batch mint + #[test] + fn test_batch_mint() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = Addr::unchecked("alice".to_string()); + let alice_raw = deps.api.addr_canonicalize(alice.as_str()).unwrap(); + let admin = Addr::unchecked("admin".to_string()); + let admin_raw = deps.api.addr_canonicalize(admin.as_str()).unwrap(); + let pub1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("NFT1".to_string()), + description: Some("pub1".to_string()), + image: Some("uri1".to_string()), + ..Extension::default() + }), + }; + let priv2 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("NFT2".to_string()), + description: Some("priv2".to_string()), + image: Some("uri2".to_string()), + ..Extension::default() + }), + }; + let mints = vec![ + Mint { + token_id: None, + owner: Some(alice.to_string()), + public_metadata: Some(pub1.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: None, + public_metadata: None, + private_metadata: Some(priv2.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some(alice.to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: None, + serial_number: None, + memo: None, + }, + Mint { + token_id: None, + owner: Some(admin.to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: None, + serial_number: None, + memo: Some("has id 3".to_string()), + }, + ]; + // test minting when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::BatchMintNft { + mints: mints.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non-minter attempt + let execute_msg = ExecuteMsg::BatchMintNft { + mints: mints.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Only designated minters are allowed to mint")); + + // sanity check + let execute_msg = ExecuteMsg::BatchMintNft { + mints: mints.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minted_vec = vec![ + "0".to_string(), + "NFT2".to_string(), + "NFT3".to_string(), + "3".to_string(), + ]; + let handle_answer: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + match handle_answer { + ExecuteAnswer::BatchMintNft { token_ids } => { + assert_eq!(token_ids, minted_vec); + } + _ => panic!("unexpected"), + } + + // verify the tokens are in the id and index maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index1: u32 = load(&map2idx, "0".as_bytes()).unwrap(); + let token_key1 = index1.to_le_bytes(); + let index2: u32 = load(&map2idx, "NFT2".as_bytes()).unwrap(); + let token_key2 = index2.to_le_bytes(); + let index3: u32 = load(&map2idx, "NFT3".as_bytes()).unwrap(); + let token_key3 = index3.to_le_bytes(); + let index4: u32 = load(&map2idx, "3".as_bytes()).unwrap(); + let token_key4 = index4.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id1: String = load(&map2id, &token_key1).unwrap(); + assert_eq!("0".to_string(), id1); + let id2: String = load(&map2id, &token_key2).unwrap(); + assert_eq!("NFT2".to_string(), id2); + let id3: String = load(&map2id, &token_key3).unwrap(); + assert_eq!("NFT3".to_string(), id3); + let id4: String = load(&map2id, &token_key4).unwrap(); + assert_eq!("3".to_string(), id4); + // verify all the token info + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &token_key1).unwrap(); + assert_eq!(token1.owner, alice_raw); + assert_eq!(token1.permissions, Vec::new()); + assert!(token1.unwrapped); + let token2: Token = json_load(&info_store, &token_key2).unwrap(); + assert_eq!(token2.owner, admin_raw); + assert_eq!(token2.permissions, Vec::new()); + assert!(token2.unwrapped); + // verify the token metadata + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta1: Metadata = load(&pub_store, &token_key1).unwrap(); + assert_eq!(pub_meta1, pub1); + let pub_meta2: Option = may_load(&pub_store, &token_key2).unwrap(); + assert!(pub_meta2.is_none()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta1: Option = may_load(&priv_store, &token_key1).unwrap(); + assert!(priv_meta1.is_none()); + let priv_meta2: Metadata = load(&priv_store, &token_key2).unwrap(); + assert_eq!(priv_meta2, priv2); + // verify owner lists + assert!(Inventory::owns(&deps.storage, &alice_raw, 0).unwrap()); + assert!(Inventory::owns(&deps.storage, &alice_raw, 2).unwrap()); + assert!(Inventory::owns(&deps.storage, &admin_raw, 1).unwrap()); + assert!(Inventory::owns(&deps.storage, &admin_raw, 3).unwrap()); + // verify mint tx was logged + let (txs, total) = get_txs(&deps.api, &deps.storage, &admin_raw, 0, 4).unwrap(); + assert_eq!(total, 4); + assert_eq!(txs[0].token_id, "3".to_string()); + assert_eq!( + txs[0].action, + TxAction::Mint { + minter: admin.clone(), + recipient: admin, + } + ); + assert_eq!(txs[0].memo, Some("has id 3".to_string())); + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID NFT2 is already in use")); + } + + // test minting + #[test] + fn test_mint() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + // test minting when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test setting both token_uri and extension + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: Some("uri".to_string()), + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Metadata can not have BOTH token_uri AND extension")); + + // test non-minter attempt + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Only designated minters are allowed to mint")); + + // sanity check + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let pub_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }; + let priv_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFTpriv".to_string()), + description: Some("Nifty".to_string()), + image: Some("privuri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub_expect.clone()), + private_metadata: Some(priv_expect.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minted = extract_log(handle_result); + assert!(minted.contains("MyNFT")); + // verify the token is in the id and index maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // verify all the token info + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &token_key).unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let admin_raw = deps.api.addr_canonicalize("admin").unwrap(); + assert_eq!(token.owner, alice_raw); + assert_eq!(token.permissions, Vec::new()); + assert!(token.unwrapped); + // verify the token metadata + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); + assert_eq!(pub_meta, pub_expect); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); + assert_eq!(priv_meta, priv_expect); + // verify token is in owner list + assert!(Inventory::owns(&deps.storage, &alice_raw, 0).unwrap()); + // verify mint tx was logged to both parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Mint { + minter: Addr::unchecked("admin".to_string()), + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!(txs[0].memo, Some("Mint it baby!".to_string())); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &admin_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs, tx2); + // test minting with an existing token id + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFTpriv".to_string()), + description: Some("Nifty".to_string()), + image: Some("privuri".to_string()), + ..Extension::default() + }), + }), + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID MyNFT is already in use")); + + // test minting without specifying recipient or id + let pub_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("AdminNFT".to_string()), + description: None, + image: None, + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: None, + owner: None, + public_metadata: Some(pub_expect.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Admin wants his own".to_string()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minted_str = "1".to_string(); + let handle_answer: ExecuteAnswer = + from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + match handle_answer { + ExecuteAnswer::MintNft { token_id } => { + assert_eq!(token_id, minted_str); + } + _ => panic!("unexpected"), + } + + // verify token is in the token list + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "1".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("1".to_string(), id); + // verify token info + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &token_key).unwrap(); + let admin_raw = deps.api.addr_canonicalize("admin").unwrap(); + assert_eq!(token.owner, admin_raw); + assert_eq!(token.permissions, Vec::new()); + assert!(token.unwrapped); + // verify metadata + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); + assert_eq!(pub_meta, pub_expect); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &token_key).unwrap(); + assert!(priv_meta.is_none()); + // verify token is in the owner list + assert!(Inventory::owns(&deps.storage, &admin_raw, 1).unwrap()); + // verify mint tx was logged + let (txs, total) = get_txs(&deps.api, &deps.storage, &admin_raw, 0, 10).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs.len(), 2); + assert_eq!(txs[0].token_id, "1".to_string()); + assert_eq!( + txs[0].action, + TxAction::Mint { + minter: Addr::unchecked("admin".to_string()), + recipient: Addr::unchecked("admin".to_string()), + } + ); + assert_eq!(txs[0].memo, Some("Admin wants his own".to_string())); + assert_eq!(txs[1].token_id, "MyNFT".to_string()); + assert_eq!( + txs[1].action, + TxAction::Mint { + minter: Addr::unchecked("admin".to_string()), + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!(txs[1].memo, Some("Mint it baby!".to_string())); + } + + // test updating public metadata + #[test] + fn test_set_public_metadata() { + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "SNIP20".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: SNIP20 not found")); + + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "SNIP20".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata of token SNIP20")); + + // test setting metadata when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test not minter nor owner + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata")); + + // test owner tries but not allowed to change metadata + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata")); + + // test minter tries, but not allowed + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata")); + + // sanity check: minter updates + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let set_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(set_expect.clone()), + private_metadata: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &0u32.to_le_bytes()).unwrap(); + assert_eq!(pub_meta, set_expect); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &0u32.to_le_bytes()).unwrap(); + assert!(priv_meta.is_none()); + } + + #[test] + fn test_set_private_metadata() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "SNIP20".to_string(), + public_metadata: None, + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata of token SNIP20")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to change sealed metadata before it has been unwrapped + let (init_result, mut deps) = + init_helper_with_config(true, false, true, true, true, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: None, + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The private metadata of a sealed token can not be modified")); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "SNIP20".to_string(), + public_metadata: None, + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: SNIP20 not found")); + + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &token_key).unwrap(); + assert!(token.unwrapped); + + // test setting both token_uri and extension + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + private_metadata: Some(Metadata { + token_uri: Some("uri".to_string()), + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Metadata can not have BOTH token_uri AND extension")); + + // sanity check, minter changing metadata after owner unwrapped + let set_pub = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name Pub".to_string()), + description: Some("Minter changed the public metadata".to_string()), + image: Some("new uri pub".to_string()), + ..Extension::default() + }), + }; + let set_priv = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name Priv".to_string()), + description: Some("Minter changed the private metadata".to_string()), + image: Some("new uri priv".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: Some(set_pub.clone()), + private_metadata: Some(set_priv.clone()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); + assert_eq!(pub_meta, set_pub); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); + assert_eq!(priv_meta, set_priv); + + // test setting metadata when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: None, + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test owner trying when not authorized + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: None, + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("I changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Not authorized to update metadata of token MyNFT")); + + // test authorized owner creates new metadata when it didn't exist before + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, true, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let pub_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: None, + image: Some("uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub_expect.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let priv_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("New Name".to_string()), + description: Some("Owner changed the metadata".to_string()), + image: Some("new uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::SetMetadata { + token_id: "MyNFT".to_string(), + public_metadata: None, + private_metadata: Some(priv_expect.clone()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); + assert_eq!(priv_meta, priv_expect); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); + assert_eq!(pub_meta, pub_expect); + } + + // test Reveal + #[test] + fn test_reveal() { + let (init_result, mut deps) = + init_helper_with_config(true, false, true, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, true, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token MyNFT")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test reveal when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MySealedNFT".to_string()), + description: Some("Sealed metadata test".to_string()), + image: Some("sealed_uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test sealed metadata not enabled + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Sealed metadata functionality is not enabled for this contract")); + + // test someone other than owner tries to unwrap + let (init_result, mut deps) = + init_helper_with_config(false, false, true, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let seal_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MySealedNFT".to_string()), + description: Some("Sealed metadata test".to_string()), + image: Some("sealed_uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(seal_meta.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token MyNFT")); + + // sanity check, unwrap to public metadata + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &token_key).unwrap(); + assert!(priv_meta.is_none()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &token_key).unwrap(); + assert_eq!(pub_meta, seal_meta.clone()); + + // test trying to unwrap token that has already been unwrapped + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("This token has already been unwrapped")); + + // sanity check, unwrap but keep private + let (init_result, mut deps) = + init_helper_with_config(false, false, true, true, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(seal_meta.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Reveal { + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &token_key).unwrap(); + assert_eq!(priv_meta, seal_meta); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &token_key).unwrap(); + assert!(pub_meta.is_none()); + } + + // test owner setting approval for specific addresses + #[test] + fn test_set_whitelisted_approval() { + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token NFT1")); + + let pub1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Public 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub1.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let pub2: Metadata = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Public 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub2.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let pub3 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My3".to_string()), + description: Some("Public 3".to_string()), + image: Some("URI 3".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub3.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let pub4 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My4".to_string()), + description: Some("Public 4".to_string()), + image: Some("URI 4".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub4.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to set approval when status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + // setting approval is ok even during StopTransactions status + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // only allow the owner to use SetWhitelistedApproval + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token NFT1")); + + // try approving a token without specifying which token + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains( + "Attempted to grant/revoke permission for a token, but did not specify a token ID" + )); + + // try revoking a token approval without specifying which token + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::RevokeToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains( + "Attempted to grant/revoke permission for a token, but did not specify a token ID" + )); + + // sanity check + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let edmund_raw = deps.api.addr_canonicalize("edmund").unwrap(); + let frank_raw = deps.api.addr_canonicalize("frank").unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let nft1_key = 0u32.to_le_bytes(); + let nft2_key = 1u32.to_le_bytes(); + let nft3_key = 2u32.to_le_bytes(); + let nft4_key = 3u32.to_le_bytes(); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has bob with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // verify it doesn't duplicate any entries if adding permissions that already + // exist + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has bob with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // verify that setting a token approval with the same expiration as an existing ALL approval, does + // not modify THAT approval, but continues to process other permission types + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT2 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); + assert_eq!(pub_meta, pub2.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has bob with NFT2 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // verify changing an existing ALL expiration while adding token access + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm ALL permission with new expiration + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT2 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); + assert_eq!(pub_meta, pub2.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists added bob's NFT2 transfer permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // verify default expiration of "never" + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm NFT3 permissions + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists added bob's nft3 transfer permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 3); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // verify revoking a token permission that never existed doesn't break anything + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::RevokeToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm NFT4 permissions + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm AuthLists are correct + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 3); + assert!(!bob_auth.tokens[transfer_idx].contains(&3u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1500000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "edmund".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(2000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test revoking token permission + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::RevokeToken), + // expiration is ignored when only performing revoking actions + expires: Some(Expiration::AtTime(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm didn't affect ALL permissions + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 3); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1500000)) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + let edmund_oper_perm = all_perm.iter().find(|p| p.address == edmund_raw).unwrap(); + assert_eq!( + edmund_oper_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + assert_eq!(edmund_oper_perm.expirations[view_meta_idx], None); + assert_eq!(edmund_oper_perm.expirations[view_owner_idx], None); + // confirm NFT2 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft2_key).unwrap(); + assert_eq!(pub_meta, pub2); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft2_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + assert!(!token.permissions.iter().any(|p| p.address == bob_raw)); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists still has bob, but not with NFT2 transfer permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(!bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + + // test revoking a token permission when address has ALL permission removes the ALL + // permission, and adds token permissions for all the other tokens not revoked + // giving them the expiration of the removed ALL permission + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::RevokeToken), + view_private_metadata: None, + transfer: None, + expires: Some(Expiration::AtTime(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm only bob's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + assert!(!all_perm.iter().any(|p| p.address == bob_raw)); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1500000)) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + let edmund_oper_perm = all_perm.iter().find(|p| p.address == edmund_raw).unwrap(); + assert_eq!( + edmund_oper_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + assert_eq!(edmund_oper_perm.expirations[view_meta_idx], None); + assert_eq!(edmund_oper_perm.expirations[view_owner_idx], None); + // confirm NFT1 permission added view_owner for bob with the old ALL permission + // expiration, and did not touch the existing transfer permission for bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + // confirm NFT2 permission for bob and charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[transfer_idx], None); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + // confirm NFT3 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT4 permission for bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(1000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[transfer_idx], None); + // confirm AuthLists still has bob, but not with NFT3 view_owner permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_owner_idx].len(), 3); + assert!(bob_auth.tokens[view_owner_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_owner_idx].contains(&1u32)); + assert!(!bob_auth.tokens[view_owner_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_owner_idx].contains(&3u32)); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + + // test revoking all view_owner permission + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + // will be ignored but specifying shouldn't screw anything up + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::None), + view_private_metadata: None, + transfer: None, + // will be ignored but specifying shouldn't screw anything up + expires: Some(Expiration::AtTime(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm only bob's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + assert!(!all_perm.iter().any(|p| p.address == bob_raw)); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1500000)) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + let edmund_oper_perm = all_perm.iter().find(|p| p.address == edmund_raw).unwrap(); + assert_eq!( + edmund_oper_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + assert_eq!(edmund_oper_perm.expirations[view_meta_idx], None); + assert_eq!(edmund_oper_perm.expirations[view_owner_idx], None); + // confirm NFT1 removed view_owner permission for bob, and did not touch the existing + // transfer permission for bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + // confirm NFT2 permission removed bob but left and charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + assert!(!token.permissions.iter().any(|p| p.address == bob_raw)); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + // confirm NFT3 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT4 permission removed bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm AuthLists still has bob, but only for NFT1 and 3 transfer permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + + // test if approving a token for an address that already has ALL permission does + // nothing if the given expiration is the same + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "edmund".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(2000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm edmund still has ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + let edmund_oper_perm = all_perm.iter().find(|p| p.address == edmund_raw).unwrap(); + assert_eq!( + edmund_oper_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + assert_eq!(edmund_oper_perm.expirations[view_meta_idx], None); + assert_eq!(edmund_oper_perm.expirations[view_owner_idx], None); + // confirm NFT4 permissions did not add edmund + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft4_key).unwrap(); + assert_eq!(pub_meta, pub4.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft4_key).unwrap(); + assert!(priv_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm edmund did not get added to AuthList + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + assert!(!auth_list.iter().any(|a| a.address == edmund_raw)); + + // test approving a token for an address that already has ALL permission updates that + // token's permission's expiration, removes ALL permission, and sets token permission + // for all other tokens using the ALL permission's expiration + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "edmund".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(3000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm edmund's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == edmund_raw)); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1500000)) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + // confirm NFT1 added permission for edmund, + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT2 added permission for edmund, + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == bob_raw)); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT3 added permission for edmund and that the token data did not get modified + // and did not touch the existing transfer permission for bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT4 permission added edmund with input expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + // confirm AuthLists added edmund for transferring on every tokens + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 3); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + + // test that approving a token when the address has an expired ALL permission + // deletes the ALL permission and performs like a regular ApproveToken (does not + // add approve permission to all the other tokens) + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::Never), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(2000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm davids's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let may_oper: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(may_oper.is_none()); + // confirm NFT3 did not add permission for david and that the token data did not get modified + // and did not touch the existing transfer permission for bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT4 permission added david with input expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm AuthLists added david for transferring on NFT4 + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 4); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&3u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + + // giving frank ALL permission for later test + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "frank".to_string(), + // will be ignored but specifying shouldn't screw anything up + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: None, + expires: Some(Expiration::AtHeight(5000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm frank's ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let frank_oper_perm = all_perm.iter().find(|p| p.address == frank_raw).unwrap(); + assert_eq!( + frank_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(5000)) + ); + assert_eq!(frank_oper_perm.expirations[view_meta_idx], None); + assert_eq!(frank_oper_perm.expirations[transfer_idx], None); + // confirm NFT4 did not add permission for frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft4_key).unwrap(); + assert_eq!(pub_meta, pub4); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm AuthLists did not add frank + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 4); + assert!(!auth_list.iter().any(|a| a.address == frank_raw)); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&3u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + + // test revoking a token permission when address has ALL permission removes the ALL + // permission, and adds token permissions for all the other tokens not revoked + // giving them the expiration of the removed ALL permission + // This is same as above, but testing when the address has no AuthList already + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "frank".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::RevokeToken), + view_private_metadata: None, + transfer: None, + // this will be ignored + expires: Some(Expiration::AtTime(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm frank's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm NFT1 permission added view_owner for frank with the old ALL permission + // expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 3); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + let frank_tok_perm = token + .permissions + .iter() + .find(|p| p.address == frank_raw) + .unwrap(); + assert_eq!( + frank_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(5000)) + ); + assert_eq!(frank_tok_perm.expirations[view_meta_idx], None); + assert_eq!(frank_tok_perm.expirations[transfer_idx], None); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + // confirm NFT2 permission + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 3); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + let frank_tok_perm = token + .permissions + .iter() + .find(|p| p.address == frank_raw) + .unwrap(); + assert_eq!( + frank_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(5000)) + ); + assert_eq!(frank_tok_perm.expirations[view_meta_idx], None); + assert_eq!(frank_tok_perm.expirations[transfer_idx], None); + // confirm NFT3 permissions do not include frank and that the token data did not get + // modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT4 permission + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 3); + let frank_tok_perm = token + .permissions + .iter() + .find(|p| p.address == frank_raw) + .unwrap(); + assert_eq!( + frank_tok_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(5000)) + ); + assert_eq!(frank_tok_perm.expirations[view_meta_idx], None); + assert_eq!(frank_tok_perm.expirations[transfer_idx], None); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists added frank with view_owner permissions for all butNFT3 + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 5); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&3u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + let frank_auth = auth_list.iter().find(|a| a.address == frank_raw).unwrap(); + assert_eq!(frank_auth.tokens[view_owner_idx].len(), 3); + assert!(frank_auth.tokens[view_owner_idx].contains(&0u32)); + assert!(frank_auth.tokens[view_owner_idx].contains(&1u32)); + assert!(frank_auth.tokens[view_owner_idx].contains(&3u32)); + assert!(frank_auth.tokens[view_meta_idx].is_empty()); + assert!(frank_auth.tokens[transfer_idx].is_empty()); + + // test granting ALL permission when the address has some token permissions + // This should remove all the token permissions and the AuthList + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "frank".to_string(), + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: None, + expires: Some(Expiration::AtHeight(2500)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm frank's ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let frank_oper_perm = all_perm.iter().find(|p| p.address == frank_raw).unwrap(); + assert_eq!( + frank_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtHeight(2500)) + ); + assert_eq!(frank_oper_perm.expirations[view_meta_idx], None); + assert_eq!(frank_oper_perm.expirations[transfer_idx], None); + // confirm NFT1 permission removed frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT2 permission removed frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + // confirm NFT4 permission removed frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists removed frank + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 4); + assert!(!auth_list.iter().any(|a| a.address == frank_raw)); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&3u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + + // test revoking all permissions when address has ALL + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "frank".to_string(), + token_id: None, + view_owner: Some(AccessLevel::None), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm frank's ALL permission is gone + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm NFT1 permission removed frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm NFT2 permission removed frank + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + // confirm NFT4 permission removed frank + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + assert!(!token.permissions.iter().any(|p| p.address == frank_raw)); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(3000)) + ); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists removed frank + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 4); + assert!(!auth_list.iter().any(|a| a.address == frank_raw)); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 2); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&1u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let edmund_auth = auth_list.iter().find(|a| a.address == edmund_raw).unwrap(); + assert_eq!(edmund_auth.tokens[transfer_idx].len(), 4); + assert!(edmund_auth.tokens[transfer_idx].contains(&0u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&1u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&2u32)); + assert!(edmund_auth.tokens[transfer_idx].contains(&3u32)); + assert!(edmund_auth.tokens[view_meta_idx].is_empty()); + assert!(edmund_auth.tokens[view_owner_idx].is_empty()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&3u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + + // test revoking a token which is address' last permission + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::RevokeToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm NFT2 permission removed charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + assert!(!token.permissions.iter().any(|p| p.address == charlie_raw)); + let edmund_tok_perm = token + .permissions + .iter() + .find(|p| p.address == edmund_raw) + .unwrap(); + assert_eq!(edmund_tok_perm.expirations[view_owner_idx], None); + assert_eq!(edmund_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + edmund_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(2000)) + ); + // confirm AuthLists removed charlie + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert!(!auth_list.iter().any(|a| a.address == charlie_raw)); + + // verify that storage entry for AuthLists gets removed when all are gone + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::None), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::None), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "edmund".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::None), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // verify no ALL permissions left + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm NFT1 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm NFT2 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm NFT3 permissions are empty (and info is intact) + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3.clone()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm NFT4 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert!(token.permissions.is_empty()); + // verify no AuthLists left + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // verify revoking doesn't break anything when there are no permissions + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "edmund".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::RevokeToken), + view_private_metadata: None, + transfer: Some(AccessLevel::None), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // verify no ALL permissions left + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm NFT1 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm NFT2 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm NFT3 permissions are empty (and info is intact) + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft3_key).unwrap(); + assert_eq!(pub_meta, pub3); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft3_key).unwrap(); + assert!(priv_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm NFT4 permissions are empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft4_key).unwrap(); + assert!(token.permissions.is_empty()); + // verify no AuthLists left + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + } + + // test approve from the cw721 spec + #[test] + fn test_cw721_approve() { + let (init_result, mut deps) = + init_helper_with_config(true, false, true, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::Approve { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::Approve { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") + ); + + let priv_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv_expect.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test contract status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Approve { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test unauthorized address attempt + let execute_msg = ExecuteMsg::Approve { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") + ); + + // test expired operator attempt + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("MyNFT".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(500000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(2000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Transfer authority for all tokens of alice has expired")); + + let tok_key = 0u32.to_le_bytes(); + let tok2_key = 1u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + + // test operator tries to grant permission to another operator. This should + // not do anything but end successfully + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm charlie still has ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + let charlie_oper_perm = all_perm.iter().find(|p| p.address == charlie_raw).unwrap(); + assert_eq!( + charlie_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(500000)) + ); + assert_eq!(charlie_oper_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_oper_perm.expirations[view_owner_idx], None); + // confirm token permission did not add charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm charlie did not get added to Authlist + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: operator sets approval for an expired operator + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(750000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm charlie's expired ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == charlie_raw)); + // confirm token permission added charlie with default expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + // confirm AuthList added charlie + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&0u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + + // sanity check: owner sets approval for an operator with only that one token + let execute_msg = ExecuteMsg::Approve { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(200)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob's ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm token permission added bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(200)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + // confirm AuthList added bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // used to test auto-setting individual token permissions when only one token + // of many is approved with a different expiration than an operator's expiration + let priv2 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT2".to_string()), + description: Some("metadata2".to_string()), + image: Some("uri2".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv2.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("metadata3".to_string()), + image: Some("uri3".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm david is an operator + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + let execute_msg = ExecuteMsg::Approve { + spender: "david".to_string(), + token_id: "MyNFT2".to_string(), + expires: Some(Expiration::AtHeight(300)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm david's ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm MyNFT token permission added david with ALL permission's expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.permissions.len(), 3); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm MyNFT2 token permission added david with input expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(300)) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok2_key).unwrap(); + assert_eq!(priv_meta, priv2); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok2_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm MyNFT3 token permission added david with ALL permission's expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok3_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm AuthList added david + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 3); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 3); + assert!(david_auth.tokens[transfer_idx].contains(&0u32)); + assert!(david_auth.tokens[transfer_idx].contains(&1u32)); + assert!(david_auth.tokens[transfer_idx].contains(&2u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + } + + // test Revoke from cw721 spec + #[test] + fn test_cw721_revoke() { + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::Revoke { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::Revoke { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") + ); + + let priv_expect = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv_expect.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test contract status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Revoke { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test unauthorized address attempt + let execute_msg = ExecuteMsg::Revoke { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Not authorized to grant/revoke transfer permission for token MyNFT") + ); + + // test expired operator attempt + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("MyNFT".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(500000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::Revoke { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(2000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Transfer authority for all tokens of alice has expired")); + + let tok_key = 0u32.to_le_bytes(); + let tok2_key = 1u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + + // test operator tries to revoke permission from another operator + let execute_msg = ExecuteMsg::Revoke { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(1000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Can not revoke transfer permission from an existing operator")); + + // sanity check: operator revokes approval from an expired operator will delete + // the expired ALL permission + let execute_msg = ExecuteMsg::Revoke { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(750000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm charlie's expired ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == charlie_raw)); + // confirm token permission is still empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert!(token.permissions.is_empty()); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm AuthList is still empty + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: operator approves, then revokes + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(200)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm charlie does not have ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == charlie_raw)); + // confirm token permission added charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(200)) + ); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + let execute_msg = ExecuteMsg::Revoke { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm charlie does not have ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == charlie_raw)); + // confirm token permission removed charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm AuthList removed charlie + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // verify revoking a non-existent permission does not break anything + let execute_msg = ExecuteMsg::Revoke { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm charlie does not have ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + assert!(!all_perm.iter().any(|p| p.address == charlie_raw)); + // confirm token does not list charlie + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm AuthList doesn not contain charlie + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: owner revokes token approval for an operator with only that one token + let execute_msg = ExecuteMsg::Revoke { + spender: "bob".to_string(), + token_id: "MyNFT".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob's ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm token permission is empty + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv_expect); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + assert!(token.permissions.is_empty()); + // confirm AuthList does not contain bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // used to test auto-setting individual token permissions when only one token + // of many is revoked from an operator + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let priv2 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT2".to_string()), + description: Some("metadata2".to_string()), + image: Some("uri2".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv2.clone()), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("metadata3".to_string()), + image: Some("uri3".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::Never), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm david is an operator + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let david_oper_perm = all_perm.iter().find(|p| p.address == david_raw).unwrap(); + assert_eq!( + david_oper_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_oper_perm.expirations[view_meta_idx], None); + assert_eq!(david_oper_perm.expirations[view_owner_idx], None); + let execute_msg = ExecuteMsg::Revoke { + spender: "david".to_string(), + token_id: "MyNFT2".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("alice", &[]), + execute_msg, + ); + // confirm david's ALL permission was removed + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Option> = json_may_load(&all_store, alice_key).unwrap(); + assert!(all_perm.is_none()); + // confirm MyNFT token permission added david with ALL permission's expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm MyNFT2 token permission does not contain david + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok2_key).unwrap(); + assert!(token.permissions.is_empty()); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok2_key).unwrap(); + assert_eq!(priv_meta, priv2); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok2_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm MyNFT3 token permission added david with ALL permission's expiration + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok3_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let david_tok_perm = token + .permissions + .iter() + .find(|p| p.address == david_raw) + .unwrap(); + assert_eq!( + david_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + assert_eq!(david_tok_perm.expirations[view_meta_idx], None); + assert_eq!(david_tok_perm.expirations[view_owner_idx], None); + // confirm AuthList added david + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 2); + assert!(david_auth.tokens[transfer_idx].contains(&0u32)); + assert!(!david_auth.tokens[transfer_idx].contains(&1u32)); + assert!(david_auth.tokens[transfer_idx].contains(&2u32)); + assert!(david_auth.tokens[view_meta_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + } + + // test burn + #[test] + fn test_burn() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test burn when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test when burn is disabled + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Burn functionality is not enabled for this token")); + + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("privmetadata".to_string()), + image: Some("privuri".to_string()), + ..Extension::default() + }), + }), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("pubmetadata".to_string()), + image: Some("puburi".to_string()), + ..Extension::default() + }), + }), + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test unauthorized addres + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + // test expired token approval + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to token MyNFT has expired")); + + // test expired ALL approval + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to all tokens of alice has expired")); + + let tok_key = 0u32.to_le_bytes(); + let tok2_key = 1u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + + // sanity check: operator burns + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT".to_string(), + memo: Some("Burn, baby, burn!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm token was removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "MyNFT".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &tok_key).unwrap(); + assert!(id.is_none()); + // confirm token info was deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok_key).unwrap(); + assert!(token.is_none()); + // confirm the metadata has been deleted from storage + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &tok_key).unwrap(); + assert!(priv_meta.is_none()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm the tx was logged to both parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Burn { + owner: Addr::unchecked("alice".to_string()), + burner: Some(Addr::unchecked("bob".to_string())), + } + ); + assert_eq!(txs[0].memo, Some("Burn, baby, burn!".to_string())); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &bob_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs, tx2); + // confirm charlie's AuthList was removed because the only token was burned + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm the token was removed from the owner's list + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + + let transfer_idx = PermissionType::Transfer.to_usize(); + + let priv2 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT2".to_string()), + description: Some("privmetadata2".to_string()), + image: Some("privuri2".to_string()), + ..Extension::default() + }), + }); + let pub2 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT2".to_string()), + description: Some("pubmetadata2".to_string()), + image: Some("puburi2".to_string()), + ..Extension::default() + }), + }); + // sanity check: address with token permission burns it + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: priv2, + public_metadata: pub2, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let priv3 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("privmetadata3".to_string()), + image: Some("privuri3".to_string()), + ..Extension::default() + }), + }; + let pub3 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("pubmetadata3".to_string()), + image: Some("puburi3".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv3.clone()), + public_metadata: Some(pub3.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT2".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::Approve { + spender: "david".to_string(), + token_id: "MyNFT3".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT2".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + // confirm token was removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "MyNFT2".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &1u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + // confirm token info was deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok2_key).unwrap(); + assert!(token.is_none()); + // confirm MyNFT3 is intact + let token: Token = json_load(&info_store, &tok3_key).unwrap(); + let david_perm = token.permissions.iter().find(|p| p.address == david_raw); + assert!(david_perm.is_some()); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok3_key).unwrap(); + assert_eq!(priv_meta, priv3); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &tok3_key).unwrap(); + assert_eq!(pub_meta, pub3); + // confirm the MyNFT2 metadata has been deleted from storage + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &tok2_key).unwrap(); + assert!(priv_meta.is_none()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok2_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm the tx was logged to both parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 5); + assert_eq!(txs[0].token_id, "MyNFT2".to_string()); + assert_eq!( + txs[0].action, + TxAction::Burn { + owner: Addr::unchecked("alice".to_string()), + burner: Some(Addr::unchecked("charlie".to_string())), + } + ); + assert!(txs[0].memo.is_none()); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs, tx2); + // confirm charlie's AuthList was removed because his only approved token was burned + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let charlie_auth = auth_list.iter().find(|a| a.address == charlie_raw); + assert!(charlie_auth.is_none()); + let david_auth = auth_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[transfer_idx].len(), 1); + assert!(david_auth.tokens[transfer_idx].contains(&2u32)); + // confirm the token was removed from the owner's list + assert!(!Inventory::owns(&deps.storage, &alice_raw, 1).unwrap()); + assert!(Inventory::owns(&deps.storage, &alice_raw, 2).unwrap()); + + // sanity check: owner burns + let execute_msg = ExecuteMsg::BurnNft { + token_id: "MyNFT3".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm token was removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "MyNFT3".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &2u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + // confirm token info was deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok3_key).unwrap(); + assert!(token.is_none()); + // confirm the metadata has been deleted from storage + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &tok3_key).unwrap(); + assert!(priv_meta.is_none()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok3_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm the tx was logged + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 6); + assert_eq!(txs[0].token_id, "MyNFT3".to_string()); + assert_eq!( + txs[0].action, + TxAction::Burn { + owner: Addr::unchecked("alice".to_string()), + burner: None, + } + ); + assert!(txs[0].memo.is_none()); + // confirm david's AuthList was removed because the only token was burned + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm the token was removed from the owner's list + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 2).unwrap()); + } + + // test batch burn + #[test] + fn test_batch_burn() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test burn when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "NFT1".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test when burn is disabled + let execute_msg = ExecuteMsg::BurnNft { + token_id: "NFT1".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Burn functionality is not enabled for this token")); + + // set up for batch burn test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let priv3 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("privmetadata3".to_string()), + image: Some("privuri3".to_string()), + ..Extension::default() + }), + }); + let pub3 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT3".to_string()), + description: Some("pubmetadata3".to_string()), + image: Some("puburi3".to_string()), + ..Extension::default() + }), + }); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: priv3.clone(), + public_metadata: pub3.clone(), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT7".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT8".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT7".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + // test bob burning a list, but trying to burn the same token twice + let burns = vec![ + Burn { + token_ids: vec!["NFT1".to_string(), "NFT3".to_string()], + memo: None, + }, + Burn { + token_ids: vec!["NFT6".to_string()], + memo: None, + }, + Burn { + token_ids: vec!["NFT6".to_string()], + memo: None, + }, + Burn { + token_ids: vec!["NFT8".to_string()], + memo: Some("Phew!".to_string()), + }, + ]; + let execute_msg = ExecuteMsg::BatchBurnNft { + burns, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + // because the token no longer exists after burning it, it will say you are not + // authorized if supply is private, and token not found if public + assert!(error.contains("You are not authorized to perform this action on token NFT6")); + + // set up for batch burn test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: priv3.clone(), + public_metadata: pub3.clone(), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT7".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT8".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT7".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + // test bob burning a list, but one is not authorized + let burns = vec![ + Burn { + token_ids: vec![ + "NFT1".to_string(), + "NFT3".to_string(), + "NFT6".to_string(), + "NFT2".to_string(), + ], + memo: None, + }, + Burn { + token_ids: vec!["NFT8".to_string()], + memo: Some("Phew!".to_string()), + }, + ]; + let execute_msg = ExecuteMsg::BatchBurnNft { + burns, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token NFT2")); + + // set up for batch burn test + let (init_result, mut deps) = + init_helper_with_config(false, false, true, false, false, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: Some(false), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: priv3, + public_metadata: pub3, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT7".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: Some(false), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT8".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT7".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + // test bob burning NFT1 and 3 from alice with token permission, + // burning NFT6 as the owner, + // and burning NFT7 and NFT8 with ALL permission + let burns = vec![ + Burn { + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Burn { + token_ids: vec!["NFT3".to_string()], + memo: None, + }, + Burn { + token_ids: vec!["NFT6".to_string(), "NFT7".to_string(), "NFT8".to_string()], + memo: Some("Phew!".to_string()), + }, + ]; + let execute_msg = ExecuteMsg::BatchBurnNft { + burns, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let tok1_key = 0u32.to_le_bytes(); + let tok2_key = 1u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let tok6_key = 5u32.to_le_bytes(); + let tok7_key = 6u32.to_le_bytes(); + let tok8_key = 6u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let bob_key = bob_raw.as_slice(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let charlie_key = charlie_raw.as_slice(); + // confirm correct tokens were removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "NFT1".as_bytes()).unwrap(); + assert!(index.is_none()); + let index: Option = may_load(&map2idx, "NFT2".as_bytes()).unwrap(); + assert!(index.is_some()); + let index: Option = may_load(&map2idx, "NFT3".as_bytes()).unwrap(); + assert!(index.is_none()); + let index: Option = may_load(&map2idx, "NFT4".as_bytes()).unwrap(); + assert!(index.is_some()); + let index: Option = may_load(&map2idx, "NFT5".as_bytes()).unwrap(); + assert!(index.is_some()); + let index: Option = may_load(&map2idx, "NFT6".as_bytes()).unwrap(); + assert!(index.is_none()); + let index: Option = may_load(&map2idx, "NFT7".as_bytes()).unwrap(); + assert!(index.is_none()); + let index: Option = may_load(&map2idx, "NFT8".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &0u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + let id: Option = may_load(&map2id, &1u32.to_le_bytes()).unwrap(); + assert!(id.is_some()); + let id: Option = may_load(&map2id, &2u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + let id: Option = may_load(&map2id, &3u32.to_le_bytes()).unwrap(); + assert!(id.is_some()); + let id: Option = may_load(&map2id, &4u32.to_le_bytes()).unwrap(); + assert!(id.is_some()); + let id: Option = may_load(&map2id, &5u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + let id: Option = may_load(&map2id, &6u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + let id: Option = may_load(&map2id, &7u32.to_le_bytes()).unwrap(); + assert!(id.is_none()); + // confirm token infos were deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok1_key).unwrap(); + assert!(token.is_none()); + let token: Option = json_may_load(&info_store, &tok3_key).unwrap(); + assert!(token.is_none()); + let token: Option = json_may_load(&info_store, &tok6_key).unwrap(); + assert!(token.is_none()); + let token: Option = json_may_load(&info_store, &tok7_key).unwrap(); + assert!(token.is_none()); + let token: Option = json_may_load(&info_store, &tok8_key).unwrap(); + assert!(token.is_none()); + // confirm NFT3 metadata has been deleted from storage + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &tok3_key).unwrap(); + assert!(priv_meta.is_none()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Option = may_load(&pub_store, &tok3_key).unwrap(); + assert!(pub_meta.is_none()); + // confirm NFT2 is intact + let token: Token = json_load(&info_store, &tok2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!( + charlie_tok_perm.expirations[view_meta_idx], + Some(Expiration::Never) + ); + assert_eq!(charlie_tok_perm.expirations[transfer_idx], None); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + assert_eq!(token.owner, alice_raw); + assert!(!token.unwrapped); + // confirm owner lists are correct + // alice only owns NFT2 + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + // bob owns NFT4 and NFT5 + let inventory = Inventory::new(&deps.storage, bob_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 3).unwrap()); + assert!(inventory.contains(&deps.storage, 4).unwrap()); + // charlie does not own any + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + // confirm AuthLists are correct + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + // alice gave charlie view metadata permission on NFT2 + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 1); + let charlie_auth = alice_list + .iter() + .find(|a| a.address == charlie_raw) + .unwrap(); + assert_eq!(charlie_auth.tokens[view_meta_idx].len(), 1); + assert!(charlie_auth.tokens[view_meta_idx].contains(&1u32)); + assert!(charlie_auth.tokens[transfer_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + // bob gave charlie view owner and view metadata permission on NFT5 + let bob_list: Vec = load(&auth_store, bob_key).unwrap(); + assert_eq!(bob_list.len(), 2); + let charlie_auth = bob_list.iter().find(|a| a.address == charlie_raw).unwrap(); + assert_eq!(charlie_auth.tokens[view_meta_idx].len(), 1); + assert!(charlie_auth.tokens[view_meta_idx].contains(&4u32)); + assert!(charlie_auth.tokens[transfer_idx].is_empty()); + assert_eq!(charlie_auth.tokens[view_owner_idx].len(), 1); + assert!(charlie_auth.tokens[view_owner_idx].contains(&4u32)); + // bob gave alice view owner permission on NFT4 and NFT5 + // and transfer permission on NFT5 + let alice_auth = bob_list.iter().find(|a| a.address == alice_raw).unwrap(); + assert_eq!(alice_auth.tokens[transfer_idx].len(), 1); + assert!(alice_auth.tokens[transfer_idx].contains(&4u32)); + assert!(alice_auth.tokens[view_meta_idx].is_empty()); + assert_eq!(alice_auth.tokens[view_owner_idx].len(), 2); + assert!(alice_auth.tokens[view_owner_idx].contains(&3u32)); + assert!(alice_auth.tokens[view_owner_idx].contains(&4u32)); + // charlie has no tokens so should not have any AuthLists + let charlie_list: Option> = may_load(&auth_store, charlie_key).unwrap(); + assert!(charlie_list.is_none()); + // confirm one of the txs + let (txs, total) = get_txs(&deps.api, &deps.storage, &bob_raw, 0, 3).unwrap(); + assert_eq!(total, 8); + assert_eq!(txs.len(), 3); + assert_eq!(txs[0].token_id, "NFT8".to_string()); + assert_eq!( + txs[0].action, + TxAction::Burn { + owner: Addr::unchecked("charlie".to_string()), + burner: Some(Addr::unchecked("bob".to_string())), + } + ); + assert_eq!(txs[0].memo, Some("Phew!".to_string())); + assert_eq!(txs[1].memo, Some("Phew!".to_string())); + assert_eq!(txs[2].memo, Some("Phew!".to_string())); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 1).unwrap(); + assert_eq!(total, 4); + assert_eq!(txs[0], tx2[0]); + } + + // test transfer + #[test] + fn test_transfer() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test transfer when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + recipient: "bob".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let execute_msg = ExecuteMsg::TransferNft { + recipient: "bob".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private + let execute_msg = ExecuteMsg::TransferNft { + recipient: "bob".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + let priv1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("privmetadata".to_string()), + image: Some("privuri".to_string()), + ..Extension::default() + }), + }; + let pub1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("pubmetadata".to_string()), + image: Some("puburi".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv1.clone()), + public_metadata: Some(pub1.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test unauthorized sender (but we'll give him view owner access) + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("MyNFT".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + recipient: "bob".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + // test expired token approval + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + recipient: "bob".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to token MyNFT has expired")); + + // test expired ALL approval + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + recipient: "charlie".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to all tokens of alice has expired")); + + let tok_key = 0u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let charlie_key = charlie_raw.as_slice(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let david_key = david_raw.as_slice(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + + // confirm that transfering to the same address that owns the token does not + // erase the current permissions + let execute_msg = ExecuteMsg::TransferNft { + recipient: "alice".to_string(), + token_id: "MyNFT".to_string(), + memo: Some("Xfer it".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token info is the same + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert_eq!(token.permissions.len(), 2); + let charlie_tok_perm = token + .permissions + .iter() + .find(|p| p.address == charlie_raw) + .unwrap(); + assert_eq!(charlie_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + charlie_tok_perm.expirations[transfer_idx], + Some(Expiration::AtHeight(10)) + ); + assert_eq!(charlie_tok_perm.expirations[view_owner_idx], None); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[transfer_idx], None); + assert_eq!( + bob_tok_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert!(token.unwrapped); + // confirm no transfer tx was logged (latest should be the mint tx) + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!( + txs[0].action, + TxAction::Mint { + minter: Addr::unchecked("admin".to_string()), + recipient: Addr::unchecked("alice".to_string()), + } + ); + // confirm the owner list is correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's and bob's AuthList were not changed + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 2); + let charlie_auth = alice_list + .iter() + .find(|a| a.address == charlie_raw) + .unwrap(); + assert_eq!(charlie_auth.tokens[transfer_idx].len(), 1); + assert!(charlie_auth.tokens[transfer_idx].contains(&0u32)); + assert!(charlie_auth.tokens[view_meta_idx].is_empty()); + assert!(charlie_auth.tokens[view_owner_idx].is_empty()); + let bob_auth = alice_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[view_owner_idx].len(), 1); + assert!(bob_auth.tokens[view_owner_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + + // sanity check: operator transfers + let execute_msg = ExecuteMsg::TransferNft { + recipient: "david".to_string(), + token_id: "MyNFT".to_string(), + memo: Some("Xfer it".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to david now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, david_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the metadata is intact + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv1.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("bob".to_string())), + recipient: Addr::unchecked("david".to_string()), + } + ); + assert_eq!(txs[0].memo, Some("Xfer it".to_string())); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &bob_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + let (tx3, total) = get_txs(&deps.api, &deps.storage, &david_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs, tx2); + assert_eq!(tx2, tx3); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, david_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's and bob's AuthList were removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm david did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, david_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: address with token permission xfers it to itself + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + recipient: "charlie".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + // confirm token was not removed from the list + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to charlie now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, charlie_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the metadata is intact + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv1); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); + assert_eq!(pub_meta, pub1); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 10).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("david".to_string()), + sender: Some(Addr::unchecked("charlie".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + } + ); + assert_eq!(txs[0].memo, None); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &david_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs, tx2); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, david_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's AuthList was removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, david_key).unwrap(); + assert!(auth_list.is_none()); + // confirm charlie did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, charlie_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: owner xfers + let execute_msg = ExecuteMsg::TransferNft { + recipient: "alice".to_string(), + token_id: "MyNFT".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to alice now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("charlie".to_string()), + sender: None, + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!(txs[0].memo, None); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 3); + assert_eq!(txs, tx2); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's AuthList was removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm charlie did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, charlie_key).unwrap(); + assert!(auth_list.is_none()); + } + + // test batch transfer + #[test] + fn test_batch_transfer() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test transfer when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let transfers = vec![Transfer { + recipient: "bob".to_string(), + token_ids: vec!["MyNFT".to_string()], + memo: None, + }]; + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let transfers = vec![Transfer { + recipient: "bob".to_string(), + token_ids: vec!["MyNFT".to_string()], + memo: None, + }]; + + // test token not found when supply is public + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let tok_key = 0u32.to_le_bytes(); + let tok5_key = 4u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let bob_key = bob_raw.as_slice(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let charlie_key = charlie_raw.as_slice(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + + // set up for batch transfer test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::BatchMintNft { + mints: vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT6".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + ], + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let transfers = vec![ + Transfer { + recipient: "charlie".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "alice".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "bob".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "david".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + ]; + + // test transferring the same token among address the sender has ALL permission, + // but then breaks when it gets to an address he does not have authority for + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token NFT1")); + // confirm it didn't die until david tried to transfer itaway from bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, bob_raw); + + // set up for batch transfer test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let transfers = vec![ + Transfer { + recipient: "charlie".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "alice".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "bob".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + ]; + + // test transferring the same token among address the sender has ALL permission + // and verify the AuthLists are correct after all the transfers + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "NFT1".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("NFT1".to_string(), id); + // confirm token has the correct owner and the permissions were cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, bob_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm transfer txs were logged + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 10).unwrap(); + assert_eq!(total, 6); + assert_eq!(txs.len(), 6); + assert_eq!( + txs[2].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + } + ); + assert_eq!( + txs[1].action, + TxAction::Transfer { + from: Addr::unchecked("charlie".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("bob".to_string()), + } + ); + // confirm the owner list is correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + assert!(inventory.contains(&deps.storage, 2).unwrap()); + let inventory = Inventory::new(&deps.storage, bob_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 3); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 3).unwrap()); + assert!(inventory.contains(&deps.storage, 4).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 5).unwrap()); + // confirm authLists were updated correctly + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 2); + let david_auth = alice_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[view_meta_idx].len(), 1); + assert!(david_auth.tokens[view_meta_idx].contains(&2u32)); + assert!(david_auth.tokens[transfer_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + let bob_auth = alice_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[view_owner_idx].len(), 1); + assert!(bob_auth.tokens[view_owner_idx].contains(&2u32)); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 2); + assert!(bob_auth.tokens[view_meta_idx].contains(&1u32)); + assert!(bob_auth.tokens[view_meta_idx].contains(&2u32)); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + let bob_list: Vec = load(&auth_store, bob_key).unwrap(); + assert_eq!(bob_list.len(), 1); + let alice_auth = bob_list.iter().find(|a| a.address == alice_raw).unwrap(); + assert_eq!(alice_auth.tokens[view_owner_idx].len(), 2); + assert!(alice_auth.tokens[view_owner_idx].contains(&3u32)); + assert!(alice_auth.tokens[view_owner_idx].contains(&4u32)); + assert_eq!(alice_auth.tokens[view_meta_idx].len(), 1); + assert!(alice_auth.tokens[view_meta_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[transfer_idx].len(), 1); + assert!(alice_auth.tokens[transfer_idx].contains(&3u32)); + let charlie_list: Vec = load(&auth_store, charlie_key).unwrap(); + assert_eq!(charlie_list.len(), 1); + let bob_auth = charlie_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&5u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + + let transfers = vec![ + Transfer { + recipient: "charlie".to_string(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + Transfer { + recipient: "alice".to_string(), + token_ids: vec!["NFT5".to_string()], + memo: None, + }, + Transfer { + recipient: "bob".to_string(), + token_ids: vec!["NFT3".to_string()], + memo: None, + }, + ]; + + // test bobs trnsfer two of his tokens and one of alice's + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + // confirm tokens have the correct owner and the permissions were cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, charlie_raw); + assert!(token.permissions.is_empty()); + let token: Token = json_load(&info_store, &tok3_key).unwrap(); + assert_eq!(token.owner, bob_raw); + assert!(token.permissions.is_empty()); + let token: Token = json_load(&info_store, &tok5_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.permissions.is_empty()); + // confirm the owner list is correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + assert!(inventory.contains(&deps.storage, 4).unwrap()); + let inventory = Inventory::new(&deps.storage, bob_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 2).unwrap()); + assert!(inventory.contains(&deps.storage, 3).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 5).unwrap()); + // confirm authLists were updated correctly + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 1); + let bob_auth = alice_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + let bob_list: Vec = load(&auth_store, bob_key).unwrap(); + assert_eq!(bob_list.len(), 1); + let alice_auth = bob_list.iter().find(|a| a.address == alice_raw).unwrap(); + assert_eq!(alice_auth.tokens[view_owner_idx].len(), 1); + assert!(alice_auth.tokens[view_owner_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[view_meta_idx].len(), 1); + assert!(alice_auth.tokens[view_meta_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[transfer_idx].len(), 1); + assert!(alice_auth.tokens[transfer_idx].contains(&3u32)); + let charlie_list: Vec = load(&auth_store, charlie_key).unwrap(); + assert_eq!(charlie_list.len(), 1); + let bob_auth = charlie_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&5u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + + // set up for batch transfer test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::BatchMintNft { + mints: vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT6".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + ], + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers: vec![ + Transfer { + recipient: "charlie".to_string(), + token_ids: vec!["NFT2".to_string(), "NFT3".to_string(), "NFT4".to_string()], + memo: Some("test memo".to_string()), + }, + Transfer { + recipient: "charlie".to_string(), + token_ids: vec!["NFT1".to_string(), "NFT5".to_string()], + memo: None, + }, + ], + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + // confirm charlie's tokens + let query_msg = QueryMsg::Tokens { + owner: "charlie".to_string(), + viewer: None, + viewing_key: Some("ckey".to_string()), + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec![ + "NFT6".to_string(), + "NFT2".to_string(), + "NFT3".to_string(), + "NFT4".to_string(), + "NFT1".to_string(), + "NFT5".to_string(), + ]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + let xfer4 = Tx { + tx_id: 8, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT4".to_string(), + memo: Some("test memo".to_string()), + action: TxAction::Transfer { + from: Addr::unchecked("bob".to_string()), + sender: None, + recipient: Addr::unchecked("charlie".to_string()), + }, + }; + let xfer1 = Tx { + tx_id: 9, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT1".to_string(), + memo: None, + action: TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("bob".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + }, + }; + let query_msg = QueryMsg::TransactionHistory { + address: "charlie".to_string(), + viewing_key: "ckey".to_string(), + page: None, + page_size: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!(total, 6); + assert_eq!(txs[1], xfer1); + assert_eq!(txs[2], xfer4); + } + _ => panic!("unexpected"), + } + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::Tokens { + owner: "alice".to_string(), + viewer: None, + viewing_key: Some("akey".to_string()), + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + assert!(tokens.is_empty()); + } + _ => panic!("unexpected"), + } + } + + // test send + #[test] + fn test_send() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test send when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "bob".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let execute_msg = ExecuteMsg::SendNft { + contract: "bob".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private + let execute_msg = ExecuteMsg::SendNft { + contract: "bob".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + let priv1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("privmetadata".to_string()), + image: Some("privuri".to_string()), + ..Extension::default() + }), + }; + let pub1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("pubmetadata".to_string()), + image: Some("puburi".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(priv1.clone()), + public_metadata: Some(pub1.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test unauthorized sender (but we'll give him view owner access) + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("MyNFT".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "bob".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You are not authorized to perform this action on token MyNFT")); + + // test expired token approval + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "bob".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to token MyNFT has expired")); + + // test expired ALL approval + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "charlie".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 100, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Access to all tokens of alice has expired")); + + let tok_key = 0u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let charlie_key = charlie_raw.as_slice(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let david_key = david_raw.as_slice(); + + // confirm that sending to the same address that owns the token throws an error + let execute_msg = ExecuteMsg::SendNft { + contract: "alice".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: None, + memo: Some("Xfer it".to_string()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains( + "Attempting to transfer token ID: MyNFT to the address that already owns it" + )); + + // sanity check: operator sends + // msg to go with ReceiveNft + let send_msg = Some( + to_binary(&ExecuteMsg::RevokeAll { + operator: "zoe".to_string(), + padding: None, + }) + .unwrap(), + ); + // register david's ReceiveNft + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "david code hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "david".to_string(), + receiver_info: None, + token_id: "MyNFT".to_string(), + msg: send_msg.clone(), + memo: Some("Xfer it".to_string()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("bob", &[]), + execute_msg, + ); + // confirm the receive nft msg was created + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_al = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("alice".to_string()), + token_id: "MyNFT".to_string(), + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_al = space_pad(&mut msg_fr_al.0, 256usize); + let msg_fr_al = SubMsg::new(WasmMsg::Execute { + contract_addr: "david".to_string(), + code_hash: "david code hash".to_string(), + msg: Binary(msg_fr_al.to_vec()), + funds: vec![], + }); + assert_eq!(messages[0], msg_fr_al); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to david now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, david_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the metadata is intact + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv1.clone()); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); + assert_eq!(pub_meta, pub1.clone()); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("bob".to_string())), + recipient: Addr::unchecked("david".to_string()), + } + ); + assert_eq!(txs[0].memo, Some("Xfer it".to_string())); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &bob_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + let (tx3, total) = get_txs(&deps.api, &deps.storage, &david_raw, 0, 1).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs, tx2); + assert_eq!(tx2, tx3); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, david_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's and bob's AuthList were removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm david did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, david_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: address with token permission xfers it to itself and specifies its + // code hash and that it implements BatchReceiveNft in the SendNft msg + let execute_msg = ExecuteMsg::Approve { + spender: "charlie".to_string(), + token_id: "MyNFT".to_string(), + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SendNft { + contract: "charlie".to_string(), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: "supplied in the send".to_string(), + also_implements_batch_receive_nft: Some(true), + }), + token_id: "MyNFT".to_string(), + msg: send_msg.clone(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + mock_info("charlie", &[]), + execute_msg, + ); + // confirm the batch receive nft msg was created + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_dv = to_binary(&Snip721ReceiveMsg::BatchReceiveNft { + sender: Addr::unchecked("charlie".to_string()), + from: Addr::unchecked("david".to_string()), + token_ids: vec!["MyNFT".to_string()], + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_dv = space_pad(&mut msg_fr_dv.0, 256usize); + let msg_fr_dv = SubMsg::new(WasmMsg::Execute { + contract_addr: "charlie".to_string(), + code_hash: "supplied in the send".to_string(), + msg: Binary(msg_fr_dv.to_vec()), + funds: vec![], + }); + assert_eq!(messages[0], msg_fr_dv); + // confirm token was not removed from the list + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to charlie now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, charlie_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the metadata is intact + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Metadata = load(&priv_store, &tok_key).unwrap(); + assert_eq!(priv_meta, priv1); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &tok_key).unwrap(); + assert_eq!(pub_meta, pub1); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 10).unwrap(); + assert_eq!(total, 1); + assert_eq!(txs.len(), 1); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("david".to_string()), + sender: Some(Addr::unchecked("charlie".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + } + ); + assert_eq!(txs[0].memo, None); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &david_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs, tx2); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, david_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's AuthList was removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, david_key).unwrap(); + assert!(auth_list.is_none()); + // confirm charlie did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, charlie_key).unwrap(); + assert!(auth_list.is_none()); + + // sanity check: owner sends and specifies a code hash in the send + let execute_msg = ExecuteMsg::SendNft { + contract: "alice".to_string(), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: "supplied in the send".to_string(), + also_implements_batch_receive_nft: None, + }), + token_id: "MyNFT".to_string(), + msg: send_msg.clone(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + // confirm the receive nft msg was created + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_ch = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("charlie".to_string()), + token_id: "MyNFT".to_string(), + msg: send_msg, + }) + .unwrap(); + let msg_fr_ch = space_pad(&mut msg_fr_ch.0, 256usize); + let msg_fr_ch = SubMsg::new(WasmMsg::Execute { + contract_addr: "alice".to_string(), + code_hash: "supplied in the send".to_string(), + msg: Binary(msg_fr_ch.to_vec()), + funds: vec![], + }); + assert_eq!(messages[0], msg_fr_ch); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "MyNFT".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("MyNFT".to_string(), id); + // confirm token belongs to alice now and permissions have been cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm the tx was logged to all involved parties + let (txs, total) = get_txs(&deps.api, &deps.storage, &charlie_raw, 0, 1).unwrap(); + assert_eq!(total, 2); + assert_eq!(txs[0].token_id, "MyNFT".to_string()); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("charlie".to_string()), + sender: None, + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!(txs[0].memo, None); + let (tx2, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 1).unwrap(); + assert_eq!(total, 3); + assert_eq!(txs, tx2); + // confirm both owner lists are correct + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + // confirm charlie's AuthList was removed because the only token was xferred + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + // confirm charlie did not inherit any AuthLists from the xfer + let auth_list: Option> = may_load(&auth_store, charlie_key).unwrap(); + assert!(auth_list.is_none()); + } + + // test batch send + #[test] + fn test_batch_send() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("MyNFT".to_string()), + owner: Some("alice".to_string()), + private_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("MyNFT".to_string()), + description: Some("metadata".to_string()), + image: Some("uri".to_string()), + ..Extension::default() + }), + }), + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint it baby!".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test send when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let sends = vec![Send { + contract: "bob".to_string(), + receiver_info: None, + token_ids: vec!["MyNFT".to_string()], + msg: None, + memo: None, + }]; + let execute_msg = ExecuteMsg::BatchSendNft { + sends, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::Normal, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let sends = vec![Send { + contract: "bob".to_string(), + receiver_info: None, + token_ids: vec!["MyNFT".to_string()], + msg: None, + memo: None, + }]; + + // test token not found when supply is public + let execute_msg = ExecuteMsg::BatchSendNft { + sends, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: MyNFT not found")); + + let tok_key = 0u32.to_le_bytes(); + let tok5_key = 4u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let bob_key = bob_raw.as_slice(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let charlie_key = charlie_raw.as_slice(); + let david_raw = deps.api.addr_canonicalize("david").unwrap(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + + // set up for batch send test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT4".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "alice".to_string(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT6".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "david".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "charlie code hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + // msg to go with ReceiveNft + let send_msg = Some( + to_binary(&ExecuteMsg::RevokeAll { + operator: "zoe".to_string(), + padding: None, + }) + .unwrap(), + ); + let sends = vec![ + Send { + contract: "charlie".to_string(), + receiver_info: None, + token_ids: vec!["NFT1".to_string()], + msg: send_msg.clone(), + memo: None, + }, + Send { + contract: "alice".to_string(), + receiver_info: None, + token_ids: vec!["NFT1".to_string()], + msg: send_msg.clone(), + memo: None, + }, + Send { + contract: "bob".to_string(), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: "supplied in the send".to_string(), + also_implements_batch_receive_nft: None, + }), + token_ids: vec!["NFT1".to_string()], + msg: send_msg.clone(), + memo: None, + }, + ]; + + // test sending the same token among address the sender has ALL permission + // and verify the AuthLists are correct after all the transfers + let execute_msg = ExecuteMsg::BatchSendNft { + sends, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("david", &[]), + execute_msg, + ); + // confirm the receive nft msgs were created + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_al = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("alice".to_string()), + token_id: "NFT1".to_string(), + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_al = space_pad(&mut msg_fr_al.0, 256usize); + let msg_fr_al = SubMsg::new(WasmMsg::Execute { + contract_addr: "charlie".to_string(), + code_hash: "charlie code hash".to_string(), + msg: Binary(msg_fr_al.to_vec()), + funds: vec![], + }); + assert_eq!(messages[0], msg_fr_al); + assert_eq!(messages.len(), 2); + let mut msg_fr_al = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("alice".to_string()), + token_id: "NFT1".to_string(), + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_al = space_pad(&mut msg_fr_al.0, 256usize); + let msg_fr_al = SubMsg::new(WasmMsg::Execute { + contract_addr: "bob".to_string(), + code_hash: "supplied in the send".to_string(), + msg: Binary(msg_fr_al.to_vec()), + funds: vec![], + }); + assert_eq!(messages[1], msg_fr_al); + // confirm token was not removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: u32 = load(&map2idx, "NFT1".as_bytes()).unwrap(); + let token_key = index.to_le_bytes(); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: String = load(&map2id, &token_key).unwrap(); + assert_eq!("NFT1".to_string(), id); + // confirm token has the correct owner and the permissions were cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, bob_raw); + assert!(token.permissions.is_empty()); + assert!(token.unwrapped); + // confirm transfer txs were logged + let (txs, total) = get_txs(&deps.api, &deps.storage, &alice_raw, 0, 10).unwrap(); + assert_eq!(total, 6); + assert_eq!(txs.len(), 6); + assert_eq!( + txs[2].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + } + ); + assert_eq!( + txs[1].action, + TxAction::Transfer { + from: Addr::unchecked("charlie".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("alice".to_string()), + } + ); + assert_eq!( + txs[0].action, + TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("david".to_string())), + recipient: Addr::unchecked("bob".to_string()), + } + ); + // confirm the owner list is correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + assert!(inventory.contains(&deps.storage, 2).unwrap()); + let inventory = Inventory::new(&deps.storage, bob_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 3); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 3).unwrap()); + assert!(inventory.contains(&deps.storage, 4).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 1); + assert!(inventory.contains(&deps.storage, 5).unwrap()); + // confirm authLists were updated correctly + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 2); + let david_auth = alice_list.iter().find(|a| a.address == david_raw).unwrap(); + assert_eq!(david_auth.tokens[view_meta_idx].len(), 1); + assert!(david_auth.tokens[view_meta_idx].contains(&2u32)); + assert!(david_auth.tokens[transfer_idx].is_empty()); + assert!(david_auth.tokens[view_owner_idx].is_empty()); + let bob_auth = alice_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[view_owner_idx].len(), 1); + assert!(bob_auth.tokens[view_owner_idx].contains(&2u32)); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 2); + assert!(bob_auth.tokens[view_meta_idx].contains(&1u32)); + assert!(bob_auth.tokens[view_meta_idx].contains(&2u32)); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + let bob_list: Vec = load(&auth_store, bob_key).unwrap(); + assert_eq!(bob_list.len(), 1); + let alice_auth = bob_list.iter().find(|a| a.address == alice_raw).unwrap(); + assert_eq!(alice_auth.tokens[view_owner_idx].len(), 2); + assert!(alice_auth.tokens[view_owner_idx].contains(&3u32)); + assert!(alice_auth.tokens[view_owner_idx].contains(&4u32)); + assert_eq!(alice_auth.tokens[view_meta_idx].len(), 1); + assert!(alice_auth.tokens[view_meta_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[transfer_idx].len(), 1); + assert!(alice_auth.tokens[transfer_idx].contains(&3u32)); + let charlie_list: Vec = load(&auth_store, charlie_key).unwrap(); + assert_eq!(charlie_list.len(), 1); + let bob_auth = charlie_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&5u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + + let sends = vec![ + Send { + contract: "charlie".to_string(), + receiver_info: None, + token_ids: vec!["NFT1".to_string()], + msg: send_msg.clone(), + memo: None, + }, + Send { + contract: "alice".to_string(), + receiver_info: None, + token_ids: vec!["NFT5".to_string()], + msg: send_msg.clone(), + memo: None, + }, + Send { + contract: "bob".to_string(), + receiver_info: Some(ReceiverInfo { + recipient_code_hash: "supplied in the send".to_string(), + also_implements_batch_receive_nft: Some(true), + }), + token_ids: vec!["NFT3".to_string()], + msg: send_msg.clone(), + memo: None, + }, + ]; + + // test bob transfers two of his tokens and one of alice's + let execute_msg = ExecuteMsg::BatchSendNft { + sends, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + // confirm the receive nft msgs were created + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_b = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("bob".to_string()), + token_id: "NFT1".to_string(), + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_b = space_pad(&mut msg_fr_b.0, 256usize); + let msg_fr_b = SubMsg::new(WasmMsg::Execute { + contract_addr: "charlie".to_string(), + code_hash: "charlie code hash".to_string(), + msg: Binary(msg_fr_b.to_vec()), + funds: vec![], + }); + assert_eq!(messages[0], msg_fr_b); + assert_eq!(messages.len(), 2); + let mut msg_fr_al = to_binary(&Snip721ReceiveMsg::BatchReceiveNft { + sender: Addr::unchecked("bob".to_string()), + from: Addr::unchecked("alice".to_string()), + token_ids: vec!["NFT3".to_string()], + msg: send_msg, + }) + .unwrap(); + let msg_fr_al = space_pad(&mut msg_fr_al.0, 256usize); + let msg_fr_al = SubMsg::new(WasmMsg::Execute { + contract_addr: "bob".to_string(), + code_hash: "supplied in the send".to_string(), + msg: Binary(msg_fr_al.to_vec()), + funds: vec![], + }); + assert_eq!(messages[1], msg_fr_al); + // confirm tokens have the correct owner and the permissions were cleared + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &tok_key).unwrap(); + assert_eq!(token.owner, charlie_raw); + assert!(token.permissions.is_empty()); + let token: Token = json_load(&info_store, &tok3_key).unwrap(); + assert_eq!(token.owner, bob_raw); + assert!(token.permissions.is_empty()); + let token: Token = json_load(&info_store, &tok5_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.permissions.is_empty()); + // confirm the owner list is correct + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + assert!(inventory.contains(&deps.storage, 4).unwrap()); + let inventory = Inventory::new(&deps.storage, bob_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 2).unwrap()); + assert!(inventory.contains(&deps.storage, 3).unwrap()); + let inventory = Inventory::new(&deps.storage, charlie_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 5).unwrap()); + // confirm authLists were updated correctly + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let alice_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(alice_list.len(), 1); + let bob_auth = alice_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + let bob_list: Vec = load(&auth_store, bob_key).unwrap(); + assert_eq!(bob_list.len(), 1); + let alice_auth = bob_list.iter().find(|a| a.address == alice_raw).unwrap(); + assert_eq!(alice_auth.tokens[view_owner_idx].len(), 1); + assert!(alice_auth.tokens[view_owner_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[view_meta_idx].len(), 1); + assert!(alice_auth.tokens[view_meta_idx].contains(&3u32)); + assert_eq!(alice_auth.tokens[transfer_idx].len(), 1); + assert!(alice_auth.tokens[transfer_idx].contains(&3u32)); + let charlie_list: Vec = load(&auth_store, charlie_key).unwrap(); + assert_eq!(charlie_list.len(), 1); + let bob_auth = charlie_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + assert_eq!(bob_auth.tokens[view_meta_idx].len(), 1); + assert!(bob_auth.tokens[view_meta_idx].contains(&5u32)); + assert!(bob_auth.tokens[transfer_idx].is_empty()); + + // set up for batch send test + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::BatchMintNft { + mints: vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT4".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT5".to_string()), + owner: Some("bob".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT6".to_string()), + owner: Some("charlie".to_string()), + private_metadata: None, + public_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + ], + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "alice code hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "charlie code hash".to_string(), + also_implements_batch_receive_nft: Some(true), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let send_msg = Some( + to_binary(&ExecuteMsg::RevokeAll { + operator: "zoe".to_string(), + padding: None, + }) + .unwrap(), + ); + let execute_msg = ExecuteMsg::BatchSendNft { + sends: vec![ + Send { + contract: "charlie".to_string(), + receiver_info: None, + token_ids: vec!["NFT2".to_string(), "NFT3".to_string(), "NFT4".to_string()], + msg: send_msg.clone(), + memo: Some("test memo".to_string()), + }, + Send { + contract: "alice".to_string(), + receiver_info: None, + token_ids: vec!["NFT3".to_string(), "NFT4".to_string(), "NFT6".to_string()], + msg: None, + memo: None, + }, + ], + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let handle_resp = handle_result.unwrap(); + let messages = handle_resp.messages; + let mut msg_fr_al = to_binary(&Snip721ReceiveMsg::BatchReceiveNft { + sender: Addr::unchecked("bob".to_string()), + from: Addr::unchecked("alice".to_string()), + token_ids: vec!["NFT2".to_string(), "NFT3".to_string()], + msg: send_msg.clone(), + }) + .unwrap(); + let msg_fr_al = space_pad(&mut msg_fr_al.0, 256usize); + let msg_fr_al = SubMsg::new(WasmMsg::Execute { + contract_addr: "charlie".to_string(), + code_hash: "charlie code hash".to_string(), + msg: Binary(msg_fr_al.to_vec()), + funds: vec![], + }); + let mut msf_fr_b = to_binary(&Snip721ReceiveMsg::BatchReceiveNft { + sender: Addr::unchecked("bob".to_string()), + from: Addr::unchecked("bob".to_string()), + token_ids: vec!["NFT4".to_string()], + msg: send_msg, + }) + .unwrap(); + let msg_fr_b = space_pad(&mut msf_fr_b.0, 256usize); + let msg_fr_b = SubMsg::new(WasmMsg::Execute { + contract_addr: "charlie".to_string(), + code_hash: "charlie code hash".to_string(), + msg: Binary(msg_fr_b.to_vec()), + funds: vec![], + }); + let mut msg_fr_c3 = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("charlie".to_string()), + token_id: "NFT3".to_string(), + msg: None, + }) + .unwrap(); + let msg_fr_c3 = space_pad(&mut msg_fr_c3.0, 256usize); + let msg_fr_c3 = SubMsg::new(WasmMsg::Execute { + contract_addr: "alice".to_string(), + code_hash: "alice code hash".to_string(), + msg: Binary(msg_fr_c3.to_vec()), + funds: vec![], + }); + let mut msg_fr_c4 = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("charlie".to_string()), + token_id: "NFT4".to_string(), + msg: None, + }) + .unwrap(); + let msg_fr_c4 = space_pad(&mut msg_fr_c4.0, 256usize); + let msg_fr_c4 = SubMsg::new(WasmMsg::Execute { + contract_addr: "alice".to_string(), + code_hash: "alice code hash".to_string(), + msg: Binary(msg_fr_c4.to_vec()), + funds: vec![], + }); + let mut msg_fr_c6 = to_binary(&Snip721ReceiveMsg::ReceiveNft { + sender: Addr::unchecked("charlie".to_string()), + token_id: "NFT6".to_string(), + msg: None, + }) + .unwrap(); + let msg_fr_c6 = space_pad(&mut msg_fr_c6.0, 256usize); + let msg_fr_c6 = SubMsg::new(WasmMsg::Execute { + contract_addr: "alice".to_string(), + code_hash: "alice code hash".to_string(), + msg: Binary(msg_fr_c6.to_vec()), + funds: vec![], + }); + let expected_msgs = vec![msg_fr_al, msg_fr_b, msg_fr_c3, msg_fr_c4, msg_fr_c6]; + assert_eq!(messages, expected_msgs); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm alice's tokens + let query_msg = QueryMsg::Tokens { + owner: "alice".to_string(), + viewer: None, + viewing_key: Some("akey".to_string()), + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec![ + "NFT1".to_string(), + "NFT3".to_string(), + "NFT4".to_string(), + "NFT6".to_string(), + ]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + let xfer6 = Tx { + tx_id: 11, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT6".to_string(), + memo: None, + action: TxAction::Transfer { + from: Addr::unchecked("charlie".to_string()), + sender: Some(Addr::unchecked("bob".to_string())), + recipient: Addr::unchecked("alice".to_string()), + }, + }; + let xfer3 = Tx { + tx_id: 7, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT3".to_string(), + memo: Some("test memo".to_string()), + action: TxAction::Transfer { + from: Addr::unchecked("alice".to_string()), + sender: Some(Addr::unchecked("bob".to_string())), + recipient: Addr::unchecked("charlie".to_string()), + }, + }; + let query_msg = QueryMsg::TransactionHistory { + address: "alice".to_string(), + viewing_key: "akey".to_string(), + page: None, + page_size: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!(total, 8); + assert_eq!(txs[3], xfer3); + assert_eq!(txs[0], xfer6); + } + _ => panic!("unexpected"), + } + let query_msg = QueryMsg::Tokens { + owner: "charlie".to_string(), + viewer: None, + viewing_key: Some("ckey".to_string()), + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT2".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + } + + // test register receive_nft + #[test] + fn test_register_receive_nft() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test register when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "alice code hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still register when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // sanity check + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "alice code hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_RECEIVERS); + let hash: String = load( + &store, + deps.api.addr_canonicalize("alice").unwrap().as_slice(), + ) + .unwrap(); + assert_eq!(&hash, "alice code hash"); + } + + // test create viewing key + #[test] + fn test_create_viewing_key() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test creating a key when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::CreateViewingKey { + entropy: "blah".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still create a key when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::CreateViewingKey { + entropy: "blah".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!( + handle_result.is_ok(), + "handle() failed: {}", + handle_result.err().unwrap() + ); + let answer: ExecuteAnswer = from_binary(&handle_result.unwrap().data.unwrap()).unwrap(); + + let key_str = match answer { + ExecuteAnswer::ViewingKey { key } => key, + _ => panic!("NOPE"), + }; + assert!(ViewingKey::check(&deps.storage, "alice", &key_str).is_ok()); + } + + // test set viewing key + #[test] + fn test_set_viewing_key() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test setting a key when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "blah".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still set a key when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "blah".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(ViewingKey::check(&deps.storage, "alice", "blah").is_ok()); + } + + // test add minters + #[test] + fn test_add_minters() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test adding minters when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minters = vec![ + "alice".to_string(), + "bob".to_string(), + "bob".to_string(), + "alice".to_string(), + ]; + let execute_msg = ExecuteMsg::AddMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still add minters when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non admin trying to add minters + let execute_msg = ExecuteMsg::AddMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("This is an admin command and can only be run from the admin address") + ); + + // sanity check + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let admin_raw = deps.api.addr_canonicalize("admin").unwrap(); + // verify the minters we will add are not already in the list + assert!(!cur_minter.contains(&alice_raw)); + assert!(!cur_minter.contains(&bob_raw)); + let execute_msg = ExecuteMsg::AddMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the new minters were added + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 3); + assert!(cur_minter.contains(&alice_raw)); + assert!(cur_minter.contains(&bob_raw)); + assert!(cur_minter.contains(&admin_raw)); + + // let's try an empty list to see if it breaks + let minters = vec![]; + let execute_msg = ExecuteMsg::AddMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify it's the same list + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 3); + assert!(cur_minter.contains(&alice_raw)); + assert!(cur_minter.contains(&bob_raw)); + assert!(cur_minter.contains(&admin_raw)); + } + + // test remove minters + #[test] + fn test_remove_minters() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test removing minters when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minters = vec![ + "alice".to_string(), + "bob".to_string(), + "charlie".to_string(), + "bob".to_string(), + ]; + let execute_msg = ExecuteMsg::RemoveMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still remove minters when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non admin trying to remove minters + let execute_msg = ExecuteMsg::RemoveMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("This is an admin command and can only be run from the admin address") + ); + + // sanity check + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let admin_raw = deps.api.addr_canonicalize("admin").unwrap(); + let execute_msg = ExecuteMsg::AddMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the new minters were added + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 4); + assert!(cur_minter.contains(&alice_raw)); + assert!(cur_minter.contains(&bob_raw)); + assert!(cur_minter.contains(&charlie_raw)); + assert!(cur_minter.contains(&admin_raw)); + + // let's give it an empty list to see if it breaks + let minters = vec![]; + let execute_msg = ExecuteMsg::RemoveMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify it is the same list + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 4); + assert!(cur_minter.contains(&alice_raw)); + assert!(cur_minter.contains(&bob_raw)); + assert!(cur_minter.contains(&charlie_raw)); + assert!(cur_minter.contains(&admin_raw)); + + // let's throw some repeats to see if it breaks + let minters = vec![ + "alice".to_string(), + "bob".to_string(), + "alice".to_string(), + "charlie".to_string(), + ]; + let execute_msg = ExecuteMsg::RemoveMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the minters were removed + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 1); + assert!(!cur_minter.contains(&alice_raw)); + assert!(!cur_minter.contains(&bob_raw)); + assert!(!cur_minter.contains(&charlie_raw)); + assert!(cur_minter.contains(&admin_raw)); + + // let's remove the last one + let execute_msg = ExecuteMsg::RemoveMinters { + minters: vec!["admin".to_string()], + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the minters were removed + let cur_minter: Option> = may_load(&deps.storage, MINTERS_KEY).unwrap(); + assert!(cur_minter.is_none()); + } + + // test set minters + #[test] + fn test_set_minters() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test setting minters when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let minters = vec![ + "alice".to_string(), + "bob".to_string(), + "charlie".to_string(), + "bob".to_string(), + "alice".to_string(), + ]; + let execute_msg = ExecuteMsg::SetMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still set minters when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non admin trying to set minters + let execute_msg = ExecuteMsg::SetMinters { + minters: minters.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("This is an admin command and can only be run from the admin address") + ); + + // sanity check + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let execute_msg = ExecuteMsg::SetMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the new minters were added + let cur_minter: Vec = load(&deps.storage, MINTERS_KEY).unwrap(); + assert_eq!(cur_minter.len(), 3); + assert!(cur_minter.contains(&alice_raw)); + assert!(cur_minter.contains(&bob_raw)); + assert!(cur_minter.contains(&charlie_raw)); + // let's try an empty list + let minters = vec![]; + let execute_msg = ExecuteMsg::SetMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify the minters were removed + let cur_minter: Option> = may_load(&deps.storage, MINTERS_KEY).unwrap(); + assert!(cur_minter.is_none()); + } + + // test change admin + #[test] + fn test_change_admin() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test changing admin when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::ChangeAdmin { + address: "alice".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still change admin when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non admin trying to change admin + let execute_msg = ExecuteMsg::ChangeAdmin { + address: "alice".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("This is an admin command and can only be run from the admin address") + ); + + // sanity check + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let admin_raw = deps.api.addr_canonicalize("admin").unwrap(); + // verify admin is the current admin + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.admin, admin_raw); + // change it to alice + let execute_msg = ExecuteMsg::ChangeAdmin { + address: "alice".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify admin was changed + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.admin, alice_raw); + } + + // test set contract status + #[test] + fn test_set_contract_status() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test non admin trying to change status + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("This is an admin command and can only be run from the admin address") + ); + + // sanity check + // verify current status is normal + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + // change it to StopAll + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + // verify status was changed + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::StopAll.to_u8()); + } + + // test approve_all from the cw721 spec + #[test] + fn test_cw721_approve_all() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to ApproveAll when status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + // setting approval is ok even during StopTransactions status + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let nft1_key = 0u32.to_le_bytes(); + let nft2_key = 1u32.to_le_bytes(); + let nft3_key = 2u32.to_le_bytes(); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // confirm bob has transfer token permissions but not transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT1 permission has bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT2 permission has bob + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT3 permission has bob + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm AuthLists has bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 3); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // test that ApproveAll will remove all the token permissions + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob has transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_oper_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm bob's NFT1 permission is gone + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm bob's NFT2 permission is gone + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm bob's NFT3 permission is gone + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm AuthLists no longer have bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + } + + // test revoke_all from the cw721 spec + #[test] + fn test_cw721_revoke_all() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to RevokeAll when status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::RevokeAll { + operator: "bob".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + // setting approval is ok even during StopTransactions status + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + let nft1_key = 0u32.to_le_bytes(); + let nft2_key = 1u32.to_le_bytes(); + let nft3_key = 2u32.to_le_bytes(); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // confirm bob has transfer token permissions but not transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm NFT1 permission has bob + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT2 permission has bob + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm NFT3 permission has bob + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // confirm AuthLists has bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 3); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&1u32)); + assert!(bob_auth.tokens[transfer_idx].contains(&2u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + + // test that RevokeAll will remove all the token permissions + let execute_msg = ExecuteMsg::RevokeAll { + operator: "bob".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob does not have transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + // confirm bob's NFT1 permission is gone + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm bob's NFT2 permission is gone + let token: Token = json_load(&info_store, &nft2_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm bob's NFT3 permission is gone + let token: Token = json_load(&info_store, &nft3_key).unwrap(); + assert!(token.permissions.is_empty()); + // confirm AuthLists no longer have bob + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Option> = may_load(&auth_store, alice_key).unwrap(); + assert!(auth_list.is_none()); + + // grant bob transfer all permission to test if revoke all removes it + let execute_msg = ExecuteMsg::ApproveAll { + operator: "bob".to_string(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob has transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!( + bob_oper_perm.expirations[transfer_idx], + Some(Expiration::Never) + ); + // now get rid of it + let execute_msg = ExecuteMsg::RevokeAll { + operator: "bob".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm bob no longer has transfer all permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::Never) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + } + + // test making ownership private + #[test] + fn test_make_ownership_private() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test setting privacy when status prevents it + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + + // you can still set privacy when transactions are stopped + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // sanity check when contract default is private + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_OWNER_PRIV); + let owner_priv: Option = may_load(&store, alice_key).unwrap(); + assert!(owner_priv.is_none()); + + // test when contract default is public + let (init_result, mut deps) = + init_helper_with_config(false, true, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_OWNER_PRIV); + let owner_priv: bool = load(&store, alice_key).unwrap(); + assert!(!owner_priv); + } + + // test owner setting global approvals + #[test] + fn test_set_global_approval() { + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is public + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token does not exist when supply is private + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token NFT1")); + + let pub1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Pub 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(pub1.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to set approval when status does not allow + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopAll, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The contract admin has temporarily disabled this action")); + // setting approval is ok even during StopTransactions status + let execute_msg = ExecuteMsg::SetContractStatus { + level: ContractStatus::StopTransactions, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // only allow the owner to use SetGlobalApproval + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("You do not own token NFT1")); + + // try approving a token without specifying which token + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains( + "Attempted to grant/revoke permission for a token, but did not specify a token ID" + )); + + // try revoking a token approval without specifying which token + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::RevokeToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains( + "Attempted to grant/revoke permission for a token, but did not specify a token ID" + )); + + // sanity check + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let global_raw = CanonicalAddr(Binary::from(b"public")); + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let alice_key = alice_raw.as_slice(); + let view_owner_idx = PermissionType::ViewOwner.to_usize(); + let view_meta_idx = PermissionType::ViewMetadata.to_usize(); + let transfer_idx = PermissionType::Transfer.to_usize(); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 1); + let global_perm = all_perm.iter().find(|p| p.address == global_raw).unwrap(); + assert_eq!( + global_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_perm.expirations[view_meta_idx], None); + assert_eq!(global_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions and that the token data did not get modified + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let nft1_key = 0u32.to_le_bytes(); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.owner, alice_raw); + assert!(token.unwrapped); + let pub_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PUB_META); + let pub_meta: Metadata = load(&pub_store, &nft1_key).unwrap(); + assert_eq!(pub_meta, pub1); + let priv_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_PRIV_META); + let priv_meta: Option = may_load(&priv_store, &nft1_key).unwrap(); + assert!(priv_meta.is_none()); + assert_eq!(token.permissions.len(), 1); + let global_tok_perm = token + .permissions + .iter() + .find(|p| p.address == global_raw) + .unwrap(); + assert_eq!( + global_tok_perm.expirations[view_meta_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_tok_perm.expirations[transfer_idx], None); + assert_eq!(global_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has public with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let global_auth = auth_list.iter().find(|a| a.address == global_raw).unwrap(); + assert_eq!(global_auth.tokens[view_meta_idx].len(), 1); + assert!(global_auth.tokens[view_meta_idx].contains(&0u32)); + assert!(global_auth.tokens[transfer_idx].is_empty()); + assert!(global_auth.tokens[view_owner_idx].is_empty()); + + // bob approvals to make sure whitelisted addresses don't break + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + let global_perm = all_perm.iter().find(|p| p.address == global_raw).unwrap(); + assert_eq!( + global_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_perm.expirations[view_meta_idx], None); + assert_eq!(global_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!( + global_tok_perm.expirations[view_meta_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_tok_perm.expirations[transfer_idx], None); + assert_eq!(global_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has bob with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let global_auth = auth_list.iter().find(|a| a.address == global_raw).unwrap(); + assert_eq!(global_auth.tokens[view_meta_idx].len(), 1); + assert!(global_auth.tokens[view_meta_idx].contains(&0u32)); + assert!(global_auth.tokens[transfer_idx].is_empty()); + assert!(global_auth.tokens[view_owner_idx].is_empty()); + + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + let global_perm = all_perm.iter().find(|p| p.address == global_raw).unwrap(); + assert_eq!( + global_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_perm.expirations[view_meta_idx], None); + assert_eq!(global_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 2); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + assert_eq!( + global_tok_perm.expirations[view_meta_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_tok_perm.expirations[transfer_idx], None); + assert_eq!(global_tok_perm.expirations[view_owner_idx], None); + // confirm AuthLists has bob with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 2); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let global_auth = auth_list.iter().find(|a| a.address == global_raw).unwrap(); + assert_eq!(global_auth.tokens[view_meta_idx].len(), 1); + assert!(global_auth.tokens[view_meta_idx].contains(&0u32)); + assert!(global_auth.tokens[transfer_idx].is_empty()); + assert!(global_auth.tokens[view_owner_idx].is_empty()); + + // test revoking global approval + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::None), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // confirm ALL permission + let all_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_ALL_PERMISSIONS); + let all_perm: Vec = json_load(&all_store, alice_key).unwrap(); + assert_eq!(all_perm.len(), 2); + let bob_oper_perm = all_perm.iter().find(|p| p.address == bob_raw).unwrap(); + assert_eq!( + bob_oper_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_oper_perm.expirations[view_meta_idx], None); + assert_eq!(bob_oper_perm.expirations[transfer_idx], None); + let global_perm = all_perm.iter().find(|p| p.address == global_raw).unwrap(); + assert_eq!( + global_perm.expirations[view_owner_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(global_perm.expirations[view_meta_idx], None); + assert_eq!(global_perm.expirations[transfer_idx], None); + // confirm NFT1 permissions + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Token = json_load(&info_store, &nft1_key).unwrap(); + assert_eq!(token.permissions.len(), 1); + let bob_tok_perm = token + .permissions + .iter() + .find(|p| p.address == bob_raw) + .unwrap(); + assert_eq!( + bob_tok_perm.expirations[transfer_idx], + Some(Expiration::AtTime(1000000)) + ); + assert_eq!(bob_tok_perm.expirations[view_meta_idx], None); + assert_eq!(bob_tok_perm.expirations[view_owner_idx], None); + let global_tok_perm = token.permissions.iter().find(|p| p.address == global_raw); + assert!(global_tok_perm.is_none()); + // confirm AuthLists has bob with NFT1 permission + let auth_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_AUTHLIST); + let auth_list: Vec = load(&auth_store, alice_key).unwrap(); + assert_eq!(auth_list.len(), 1); + let bob_auth = auth_list.iter().find(|a| a.address == bob_raw).unwrap(); + assert_eq!(bob_auth.tokens[transfer_idx].len(), 1); + assert!(bob_auth.tokens[transfer_idx].contains(&0u32)); + assert!(bob_auth.tokens[view_meta_idx].is_empty()); + assert!(bob_auth.tokens[view_owner_idx].is_empty()); + let global_auth = auth_list.iter().find(|a| a.address == global_raw); + assert!(global_auth.is_none()); + } + + // test permissioning works + #[test] + fn test_check_permission() { + let (init_result, mut deps) = + init_helper_with_config(true, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(1), + chain_id: "secret-2".to_string(), + random: None, + }; + let alice_raw = deps.api.addr_canonicalize("alice").unwrap(); + let bob_raw = deps.api.addr_canonicalize("bob").unwrap(); + let charlie_raw = deps.api.addr_canonicalize("charlie").unwrap(); + let nft1_key = 0u32.to_le_bytes(); + let nft2_key = 1u32.to_le_bytes(); + let pub1 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Pub 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub1.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let pub2 = Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Pub 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub2.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test not approved + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + + // test owner is public for the contract + let (init_result, mut deps) = + init_helper_with_config(true, true, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub1.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub2.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + true, + ); + assert!(check_perm.is_ok()); + + // test owner makes their tokens private when the contract has public ownership + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + true, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + + // test owner later makes ownership of a single token public + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + // test public approval when no address is given + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + None, + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test global approval for all tokens + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token2: Token = json_load(&info_store, &nft2_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token2: Token = json_load(&info_store, &nft2_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + // test public approval when no address is given + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + None, + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test those global permissions having expired + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(2000000), + chain_id: "secret-2".to_string(), + random: None, + }; + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(1), + chain_id: "secret-2".to_string(), + random: None, + }; + + // test whitelisted approval on a token + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT2".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token2: Token = json_load(&info_store, &nft2_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&bob_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&charlie_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + + // test approval expired + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(6), + chain_id: "secret-2".to_string(), + random: None, + }; + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&bob_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("Access to token NFT2 has expired")); + + // test owner access + let check_perm = check_permission( + deps.as_ref(), + &block, + &token2, + "NFT2", + Some(&alice_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test whitelisted approval on all tokens + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(7)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&charlie_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test whitelisted ALL permission has expired + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(7), + chain_id: "secret-2".to_string(), + random: None, + }; + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&charlie_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("Access to all tokens of alice has expired")); + + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub1.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub2.clone(), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test whitelist approval expired, but global is good on a token + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: Some(Expiration::AtTime(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "secret-2".to_string(), + random: None, + }; + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test whitelist approval expired, but global is good on ALL tokens + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + transfer: None, + expires: Some(Expiration::AtTime(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "secret-2".to_string(), + random: None, + }; + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test whitelist approval is good, but global expired on a token + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: Some(Expiration::AtTime(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "secret-2".to_string(), + random: None, + }; + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // test whitelist approval is good, but global expired on ALL tokens + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + transfer: None, + expires: Some(Expiration::AtTime(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "secret-2".to_string(), + random: None, + }; + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub1, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: pub2, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test bob has view owner approval on NFT1 and view metadata approval on ALL + // while there is global view owner approval on ALL tokens and global view metadata + // approval on NFT1 + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "bob".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::All), + transfer: None, + expires: Some(Expiration::AtTime(100)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(1), + chain_id: "secret-2".to_string(), + random: None, + }; + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + let error = extract_error_msg(check_perm); + assert!(error.contains("not approved")); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // now check where the global approvals expired + let block = BlockInfo { + height: 1, + time: Timestamp::from_seconds(50), + chain_id: "secret-2".to_string(), + random: None, + }; + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewOwner, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&bob_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + + // throw a charlie transfer approval and a view meta token approval in the mix + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(100)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: "charlie".to_string(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: Some(Expiration::AtTime(100)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token1: Token = json_load(&info_store, &nft1_key).unwrap(); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&charlie_raw), + PermissionType::Transfer, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + let check_perm = check_permission( + deps.as_ref(), + &block, + &token1, + "NFT1", + Some(&charlie_raw), + PermissionType::ViewMetadata, + &mut Vec::new(), + "not approved", + false, + ); + assert!(check_perm.is_ok()); + } +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_inventory.rs b/contracts/external/snip721-roles-impl/src/unittest_inventory.rs new file mode 100644 index 0000000..991fcee --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_inventory.rs @@ -0,0 +1,252 @@ +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{Api, StdError}; + + use crate::inventory::{Inventory, InventoryIter}; + + #[test] + fn test_inventory() { + let mut deps = mock_dependencies(); + let alice = "alice".to_string(); + let alice_raw = deps.api.addr_canonicalize(&alice).unwrap(); + let mut inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + + // test trying to remove a token when the list is empty + inventory.remove(&mut deps.storage, 100, false).unwrap(); + assert_eq!(inventory.cnt, 0); + // test to_set with empty inventory + let set = inventory.to_set(&deps.storage).unwrap(); + assert!(set.is_empty()); + // test iterator with empty inventory + let mut iter = InventoryIter::new(&inventory); + assert!(iter.next(&deps.storage).unwrap().is_none()); + + // add a token to the inventory + inventory.insert(&mut deps.storage, 100, false).unwrap(); + assert_eq!(inventory.cnt, 1); + + // test adding token already in the inventory + inventory.insert(&mut deps.storage, 100, false).unwrap(); + assert_eq!(inventory.cnt, 1); + + // add 3 more tokens + inventory.insert(&mut deps.storage, 200, false).unwrap(); + inventory.insert(&mut deps.storage, 300, false).unwrap(); + inventory.insert(&mut deps.storage, 400, false).unwrap(); + assert_eq!(inventory.cnt, 4); + + let expected = [100u32, 200, 300, 400]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + // test contains + assert!(inventory.contains(&deps.storage, 300).unwrap()); + assert!(!inventory.contains(&deps.storage, 350).unwrap()); + // test owns + assert!(Inventory::owns(&deps.storage, &alice_raw, 200).unwrap()); + assert!(!Inventory::owns(&deps.storage, &alice_raw, 250).unwrap()); + + // remove 2 tokens from the inventory + inventory.remove(&mut deps.storage, 100, false).unwrap(); + inventory.remove(&mut deps.storage, 300, false).unwrap(); + // remove one already removed + inventory.remove(&mut deps.storage, 300, false).unwrap(); + + // test saving the inventory + inventory.save(&mut deps.storage).unwrap(); + // reload it + let mut inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + + assert_eq!(inventory.cnt, 2); + + let expected = [400u32, 200]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // insert another + inventory.insert(&mut deps.storage, 500, true).unwrap(); + assert_eq!(inventory.cnt, 3); + + let expected = [400u32, 200, 500]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // insert another + inventory.insert(&mut deps.storage, 600, true).unwrap(); + assert_eq!(inventory.cnt, 4); + + let expected = [400u32, 200, 500, 600]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // insert another + inventory.insert(&mut deps.storage, 700, true).unwrap(); + // try duplicate + inventory.insert(&mut deps.storage, 700, true).unwrap(); + assert_eq!(inventory.cnt, 5); + + let expected = [400u32, 200, 500, 600, 700]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // remove one + inventory.remove(&mut deps.storage, 600, true).unwrap(); + // try duplicate + inventory.remove(&mut deps.storage, 600, true).unwrap(); + + assert_eq!(inventory.cnt, 4); + + let expected = [400u32, 200, 500, 700]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // test contains + assert!(inventory.contains(&deps.storage, 500).unwrap()); + assert!(!inventory.contains(&deps.storage, 600).unwrap()); + // test owns + assert!(Inventory::owns(&deps.storage, &alice_raw, 500).unwrap()); + assert!(!Inventory::owns(&deps.storage, &alice_raw, 600).unwrap()); + + // remove the rest + inventory.remove(&mut deps.storage, 700, true).unwrap(); + inventory.remove(&mut deps.storage, 500, true).unwrap(); + inventory.remove(&mut deps.storage, 200, true).unwrap(); + inventory.remove(&mut deps.storage, 400, true).unwrap(); + + let mut inventory = Inventory::new(&deps.storage, alice_raw).unwrap(); + + assert_eq!(inventory.cnt, 0); + // test to_set with empty inventory + let set = inventory.to_set(&deps.storage).unwrap(); + assert!(set.is_empty()); + // test iterator with empty inventory + let mut iter = InventoryIter::new(&inventory); + assert!(iter.next(&deps.storage).unwrap().is_none()); + + inventory.insert(&mut deps.storage, 800, false).unwrap(); + inventory.insert(&mut deps.storage, 900, false).unwrap(); + inventory.insert(&mut deps.storage, 1000, true).unwrap(); + inventory.insert(&mut deps.storage, 1100, false).unwrap(); + inventory.insert(&mut deps.storage, 1200, false).unwrap(); + inventory.insert(&mut deps.storage, 1300, true).unwrap(); + + assert_eq!(inventory.cnt, 6); + + let expected = [800u32, 900, 1000, 1100, 1200, 1300]; + let mut expected_set: HashSet = HashSet::new(); + expected_set.extend(expected); + // verify to_set + let set = inventory.to_set(&deps.storage).unwrap(); + assert_eq!(set, expected_set); + // verify InventoryIter + let mut iter = InventoryIter::new(&inventory); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // test start after the first element + let expected = [900u32, 1000, 1100, 1200, 1300]; + // verify InventoryIter + let mut iter = + InventoryIter::start_after(&deps.storage, &inventory, 800, "No Error").unwrap(); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // test start after a middle element + let expected = [1100u32, 1200, 1300]; + // verify InventoryIter + let mut iter = + InventoryIter::start_after(&deps.storage, &inventory, 1000, "No Error").unwrap(); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // test start after the last element + let expected: Vec = Vec::new(); + // verify InventoryIter + let mut iter = + InventoryIter::start_after(&deps.storage, &inventory, 1300, "No Error").unwrap(); + let mut iter_vec = Vec::new(); + while let Some(i) = iter.next(&deps.storage).unwrap() { + iter_vec.push(i); + } + assert_eq!(iter_vec, expected); + + // test start after non-existing element + // verify InventoryIter + let res = InventoryIter::start_after(&deps.storage, &inventory, 345, "Expect Error"); + assert_eq!( + res.err(), + Some(StdError::generic_err("Expect Error".to_string())) + ); + } +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_mint_run.rs b/contracts/external/snip721-roles-impl/src/unittest_mint_run.rs new file mode 100644 index 0000000..4fb62ce --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_mint_run.rs @@ -0,0 +1,281 @@ +#[cfg(test)] +mod tests { + use std::any::Any; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{from_binary, Addr, OwnedDeps, Response, StdError, StdResult}; + + use crate::contract::{execute, instantiate, query}; + use crate::mint_run::MintRunInfo; + use crate::msg::{ExecuteMsg, InstantiateMsg, QueryAnswer, QueryMsg}; + + // Helper functions + + fn init_helper() -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(_response) => panic!("Expected error, but had Ok response"), + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected error result {:?}", err), + }, + } + } + + // test mint run info + #[test] + fn test_mint_run_info() { + let (init_result, mut deps) = init_helper(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test non-minter attempt + let execute_msg = ExecuteMsg::MintNftClones { + mint_run_id: None, + quantity: 1, + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Only designated minters are allowed to mint")); + + // test 0 quantity + let execute_msg = ExecuteMsg::MintNftClones { + mint_run_id: None, + quantity: 0, + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Quantity can not be zero")); + + // test no mint_run_id + let execute_msg = ExecuteMsg::MintNftClones { + mint_run_id: None, + quantity: 3, + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + let instantiator = "instantiator".to_string(); + let admin = "admin".to_string(); + let run_info_0 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator.clone())), + token_creator: Some(Addr::unchecked(admin.clone())), + time_of_minting: Some(1571797419), + mint_run: None, + serial_number: Some(1), + quantity_minted_this_run: Some(3), + }; + let run_info_1 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator.clone())), + token_creator: Some(Addr::unchecked(admin.clone())), + time_of_minting: Some(1571797419), + mint_run: None, + serial_number: Some(2), + quantity_minted_this_run: Some(3), + }; + let run_info_2 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator.clone())), + token_creator: Some(Addr::unchecked(admin.clone())), + time_of_minting: Some(1571797419), + mint_run: None, + serial_number: Some(3), + quantity_minted_this_run: Some(3), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "0".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_0)); + } + _ => panic!("unexpected"), + } + let query_msg = QueryMsg::NftDossier { + token_id: "1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_1)); + } + _ => panic!("unexpected"), + } + let query_msg = QueryMsg::NftDossier { + token_id: "2".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_2)); + } + _ => panic!("unexpected"), + } + + // test mint_run_id + let execute_msg = ExecuteMsg::MintNftClones { + mint_run_id: Some("Starry Night".to_string()), + quantity: 1, + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let run_info_3 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator.clone())), + token_creator: Some(Addr::unchecked(admin.clone())), + time_of_minting: Some(1571797419), + mint_run: Some(1), + serial_number: Some(1), + quantity_minted_this_run: Some(1), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "3".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_3)); + } + _ => panic!("unexpected"), + } + + // test subsequent use of mint_run_id + let execute_msg = ExecuteMsg::MintNftClones { + mint_run_id: Some("Starry Night".to_string()), + quantity: 2, + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let run_info_4 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator.clone())), + token_creator: Some(Addr::unchecked(admin.clone())), + time_of_minting: Some(1571797419), + mint_run: Some(2), + serial_number: Some(1), + quantity_minted_this_run: Some(2), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "4".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_4)); + } + _ => panic!("unexpected"), + } + let run_info_5 = MintRunInfo { + collection_creator: Some(Addr::unchecked(instantiator)), + token_creator: Some(Addr::unchecked(admin)), + time_of_minting: Some(1571797419), + mint_run: Some(2), + serial_number: Some(2), + quantity_minted_this_run: Some(2), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "5".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { mint_run_info, .. } => { + assert_eq!(mint_run_info, Some(run_info_5)); + } + _ => panic!("unexpected"), + } + } +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_non_transferable.rs b/contracts/external/snip721-roles-impl/src/unittest_non_transferable.rs new file mode 100644 index 0000000..9c56dad --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_non_transferable.rs @@ -0,0 +1,1013 @@ +#[cfg(test)] +mod tests { + use std::any::Any; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{ + from_binary, to_binary, Addr, Api, Binary, Coin, OwnedDeps, Response, StdError, StdResult, + SubMsg, Uint128, WasmMsg, + }; + use cosmwasm_storage::ReadonlyPrefixedStorage; + + use crate::contract::{execute, instantiate, query}; + use crate::expiration::Expiration; + use crate::inventory::Inventory; + use crate::msg::{ + Burn, ContractStatus, ExecuteMsg, InstantiateConfig, InstantiateMsg, Mint, + PostInstantiateCallback, QueryAnswer, QueryMsg, Send, Transfer, + }; + use crate::royalties::{DisplayRoyalty, DisplayRoyaltyInfo, Royalty, RoyaltyInfo}; + use crate::state::{ + json_may_load, load, may_load, Config, CONFIG_KEY, PREFIX_INFOS, PREFIX_MAP_TO_ID, + PREFIX_MAP_TO_INDEX, + }; + use crate::token::{Extension, Metadata, Token}; + + // Helper functions + + fn init_helper_default() -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn init_helper_with_config( + public_token_supply: bool, + public_owner: bool, + enable_sealed_metadata: bool, + unwrapped_metadata_is_private: bool, + minter_may_update_metadata: bool, + owner_may_update_metadata: bool, + enable_burn: bool, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + + let env = mock_env(); + let init_config: InstantiateConfig = from_binary(&Binary::from( + format!( + "{{\"public_token_supply\":{}, + \"public_owner\":{}, + \"enable_sealed_metadata\":{}, + \"unwrapped_metadata_is_private\":{}, + \"minter_may_update_metadata\":{}, + \"owner_may_update_metadata\":{}, + \"enable_burn\":{}}}", + public_token_supply, + public_owner, + enable_sealed_metadata, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + enable_burn, + ) + .as_bytes(), + )) + .unwrap(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: Some(init_config), + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + #[allow(clippy::too_many_arguments)] + fn init_helper_royalties_with_config( + royalty_info: Option, + public_token_supply: bool, + public_owner: bool, + enable_sealed_metadata: bool, + unwrapped_metadata_is_private: bool, + minter_may_update_metadata: bool, + owner_may_update_metadata: bool, + enable_burn: bool, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + + let env = mock_env(); + let init_config: InstantiateConfig = from_binary(&Binary::from( + format!( + "{{\"public_token_supply\":{}, + \"public_owner\":{}, + \"enable_sealed_metadata\":{}, + \"unwrapped_metadata_is_private\":{}, + \"minter_may_update_metadata\":{}, + \"owner_may_update_metadata\":{}, + \"enable_burn\":{}}}", + public_token_supply, + public_owner, + enable_sealed_metadata, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + enable_burn, + ) + .as_bytes(), + )) + .unwrap(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info, + config: Some(init_config), + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(_response) => panic!("Expected error, but had Ok response"), + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected error result {:?}", err), + }, + } + } + + // Init tests + + #[test] + fn test_init_sanity() { + // test default + let (init_result, deps) = init_helper_default(); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(!config.token_supply_is_public); + assert!(!config.owner_is_public); + assert!(!config.sealed_metadata_is_enabled); + assert!(!config.unwrap_to_private); + assert!(config.minter_may_update_metadata); + assert!(!config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + // test config specification + let (init_result, deps) = + init_helper_with_config(true, true, true, true, false, true, false); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(config.token_supply_is_public); + assert!(config.owner_is_public); + assert!(config.sealed_metadata_is_enabled); + assert!(config.unwrap_to_private); + assert!(!config.minter_may_update_metadata); + assert!(config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + // test post init callback + let mut deps = mock_dependencies(); + let env = mock_env(); + // just picking a random short HandleMsg that wouldn't really make sense + let post_init_msg = to_binary(&ExecuteMsg::MakeOwnershipPrivate { padding: None }).unwrap(); + let post_init_send = vec![Coin { + amount: Uint128::new(100), + denom: "uscrt".to_string(), + }]; + let post_init_callback = Some(PostInstantiateCallback { + msg: post_init_msg.clone(), + contract_address: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + send: post_init_send.clone(), + }); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback, + }; + + let init_response = instantiate(deps.as_mut(), env, info, init_msg).unwrap(); + assert_eq!( + init_response.messages, + vec![SubMsg::new(WasmMsg::Execute { + msg: post_init_msg, + contract_addr: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + funds: post_init_send, + })] + ); + + // test config specification with default royalties + let royalties = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + Royalty { + recipient: "alice".to_string(), + rate: 10, + }, + Royalty { + recipient: "bob".to_string(), + rate: 5, + }, + ], + }; + + let expected_hidden = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + DisplayRoyalty { + recipient: None, + rate: 10, + }, + DisplayRoyalty { + recipient: None, + rate: 5, + }, + ], + }; + + let (init_result, deps) = init_helper_royalties_with_config( + Some(royalties), + true, + true, + true, + true, + false, + true, + false, + ); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(config.token_supply_is_public); + assert!(config.owner_is_public); + assert!(config.sealed_metadata_is_enabled); + assert!(config.unwrap_to_private); + assert!(!config.minter_may_update_metadata); + assert!(config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_hidden)); + } + _ => panic!("unexpected"), + } + } + + // Handle tests + + // test no royalties if non-transferable + #[test] + fn test_no_royalties_if_non_transferable() { + let royalties = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + Royalty { + recipient: "alice".to_string(), + rate: 10, + }, + Royalty { + recipient: "bob".to_string(), + rate: 5, + }, + ], + }; + + let (init_result, mut deps) = init_helper_royalties_with_config( + Some(royalties.clone()), + false, + false, + false, + false, + false, + false, + false, + ); + assert_eq!(init_result.unwrap(), Response::default()); + + let mints = vec![ + Mint { + token_id: Some("TrySetRoys".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: Some(royalties.clone()), + transferable: Some(false), + serial_number: None, + memo: None, + }, + Mint { + token_id: Some("TryDefaultRoys".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify there are no royalties when trying to specify on mint + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("TrySetRoys".to_string()), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert!(royalty_info.is_none()); + } + _ => panic!("unexpected"), + } + + // verify there are no royalties coming from the default + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("TryDefaultRoys".to_string()), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert!(royalty_info.is_none()); + } + _ => panic!("unexpected"), + } + + // test trying SetRoyaltyInfo on a non-transferable token + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("TryDefaultRoys".to_string()), + royalty_info: Some(royalties), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Non-transferable tokens can not be sold, so royalties are meaningless") + ); + } + + // test trying to transfer/send non-transferable + #[test] + fn test_xfer_send_non_transferable() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + + let mints = vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some(alice), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify TransferNft fails on a non-transferable token + let execute_msg = ExecuteMsg::TransferNft { + recipient: bob.clone(), + token_id: "NFT1".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT1 is non-transferable")); + + // verify BatchTransferNft fails on a non-transferable token + let transfers = vec![ + Transfer { + recipient: bob.clone(), + token_ids: vec!["NFT2".to_string()], + memo: None, + }, + Transfer { + recipient: bob.clone(), + token_ids: vec!["NFT1".to_string()], + memo: None, + }, + ]; + let execute_msg = ExecuteMsg::BatchTransferNft { + transfers, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT2 is non-transferable")); + + // verify SendNft fails on a non-transferable token + let execute_msg = ExecuteMsg::SendNft { + contract: bob.clone(), + receiver_info: None, + token_id: "NFT1".to_string(), + msg: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT1 is non-transferable")); + + // verify BatchSendNft fails on a non-transferable token + let sends = vec![ + Send { + contract: bob.clone(), + receiver_info: None, + token_ids: vec!["NFT2".to_string()], + msg: None, + memo: None, + }, + Send { + contract: bob, + receiver_info: None, + token_ids: vec!["NFT1".to_string()], + msg: None, + memo: None, + }, + ]; + let execute_msg = ExecuteMsg::BatchSendNft { + sends, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT2 is non-transferable")); + } + + // test non-transferable is always burnable + #[test] + fn test_burn_non_transferable() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let tok_key = 0u32.to_le_bytes(); + let tok2_key = 1u32.to_le_bytes(); + let tok3_key = 2u32.to_le_bytes(); + let alice = "alice".to_string(); + let alice_raw = deps.api.addr_canonicalize(&alice).unwrap(); + + let mints = vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some(alice), + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify BurnNft works on a non-transferable token even when burn is disabled + let execute_msg = ExecuteMsg::BurnNft { + token_id: "NFT1".to_string(), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + // confirm token was removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "NFT1".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &tok_key).unwrap(); + assert!(id.is_none()); + // confirm token info was deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok_key).unwrap(); + assert!(token.is_none()); + // confirm the token was removed from the owner's list + let inventory = Inventory::new(&deps.storage, alice_raw.clone()).unwrap(); + assert_eq!(inventory.cnt, 2); + assert!(!inventory.contains(&deps.storage, 0).unwrap()); + assert!(inventory.contains(&deps.storage, 1).unwrap()); + assert!(inventory.contains(&deps.storage, 2).unwrap()); + + // verify BatchBurnNft works on non-transferable tokens even when burn is disabled + let burns = vec![Burn { + token_ids: vec!["NFT2".to_string(), "NFT3".to_string()], + memo: None, + }]; + let execute_msg = ExecuteMsg::BatchBurnNft { + burns, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + // confirm tokens were removed from the maps + let map2idx = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_INDEX); + let index: Option = may_load(&map2idx, "NFT2".as_bytes()).unwrap(); + assert!(index.is_none()); + let index: Option = may_load(&map2idx, "NFT3".as_bytes()).unwrap(); + assert!(index.is_none()); + let map2id = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_MAP_TO_ID); + let id: Option = may_load(&map2id, &tok2_key).unwrap(); + assert!(id.is_none()); + let id: Option = may_load(&map2id, &tok3_key).unwrap(); + assert!(id.is_none()); + // confirm token infos were deleted from storage + let info_store = ReadonlyPrefixedStorage::new(&deps.storage, PREFIX_INFOS); + let token: Option = json_may_load(&info_store, &tok2_key).unwrap(); + assert!(token.is_none()); + let token: Option = json_may_load(&info_store, &tok3_key).unwrap(); + assert!(token.is_none()); + // confirm the tokens were removed from the owner's list + let inventory = Inventory::new(&deps.storage, alice_raw).unwrap(); + assert_eq!(inventory.cnt, 0); + assert!(!inventory.contains(&deps.storage, 1).unwrap()); + assert!(!inventory.contains(&deps.storage, 2).unwrap()); + } + + // query tests + + // test new transferable and unwrapped fields of NftDossier query + #[test] + fn test_query_nft_dossier() { + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let public_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PubDesc1".to_string()), + image: Some("PubUri1".to_string()), + ..Extension::default() + }), + }; + let private_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("PrivName1".to_string()), + description: Some("PrivDesc1".to_string()), + image: Some("PrivUri1".to_string()), + ..Extension::default() + }), + }; + let alice = "alice".to_string(); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta.clone()), + private_metadata: Some(private_meta), + royalty_info: None, + serial_number: None, + transferable: Some(false), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test viewer not given, contract has public ownership + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice))); + assert_eq!(public_metadata, Some(public_meta)); + assert!(private_metadata.is_none()); + assert_eq!( + display_private_metadata_error, + Some("You are not authorized to perform this action on token NFT1".to_string()) + ); + assert!(owner_is_public); + assert!(!transferable); + assert!(!unwrapped); + assert_eq!(public_ownership_expiration, Some(Expiration::Never)); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + assert!(token_approvals.is_none()); + assert!(inventory_approvals.is_none()); + } + _ => panic!("unexpected"), + } + } + + // test IsTransferable query + #[test] + fn test_is_transferable() { + let (init_result, deps) = + init_helper_with_config(true, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let query_msg = QueryMsg::IsTransferable { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, deps) = + init_helper_with_config(false, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private + let query_msg = QueryMsg::IsTransferable { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsTransferable { + token_is_transferable, + } => { + assert!(token_is_transferable); + } + _ => panic!("unexpected"), + } + + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let mints = vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: Some(false), + serial_number: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + transferable: None, + serial_number: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test IsTransferable on a non-transferable token + let query_msg = QueryMsg::IsTransferable { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsTransferable { + token_is_transferable, + } => { + assert!(!token_is_transferable); + } + _ => panic!("unexpected"), + } + + // test IsTransferable on a transferable token + let query_msg = QueryMsg::IsTransferable { + token_id: "NFT2".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsTransferable { + token_is_transferable, + } => { + assert!(token_is_transferable); + } + _ => panic!("unexpected"), + } + } + + // test VerifyTransferApproval query on non-transferable tokens + #[test] + fn test_verify_transfer_approval() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: Some(false), + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify that alice does not have transfer approval despite owning the non-transferable token + let query_msg = QueryMsg::VerifyTransferApproval { + token_ids: vec!["NFT1".to_string()], + address: alice, + viewing_key: "akey".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::VerifyTransferApproval { + approved_for_all, + first_unapproved_token, + } => { + assert!(!approved_for_all); + assert_eq!(first_unapproved_token.unwrap(), "NFT1".to_string()); + } + _ => panic!("unexpected"), + } + + // also verify that bob does not have transfer approval + let query_msg = QueryMsg::VerifyTransferApproval { + token_ids: vec!["NFT1".to_string()], + address: bob, + viewing_key: "bkey".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::VerifyTransferApproval { + approved_for_all, + first_unapproved_token, + } => { + assert!(!approved_for_all); + assert_eq!(first_unapproved_token, Some("NFT1".to_string())); + } + _ => panic!("unexpected"), + } + } +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_queries.rs b/contracts/external/snip721-roles-impl/src/unittest_queries.rs new file mode 100644 index 0000000..a83ff67 --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_queries.rs @@ -0,0 +1,5108 @@ +#[cfg(test)] +mod tests { + use std::any::Any; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{ + from_binary, Addr, Binary, BlockInfo, Env, OwnedDeps, Response, StdError, StdResult, + Timestamp, + }; + + use crate::contract::{execute, instantiate, query}; + use crate::expiration::Expiration; + use crate::mint_run::MintRunInfo; + use crate::msg::{ + AccessLevel, BatchNftDossierElement, Cw721Approval, ExecuteMsg, InstantiateConfig, + InstantiateMsg, Mint, QueryAnswer, QueryMsg, Snip721Approval, Tx, TxAction, ViewerInfo, + }; + use crate::token::{Extension, Metadata}; + + // Helper functions + + fn init_helper_default() -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn init_helper_with_config( + public_token_supply: bool, + public_owner: bool, + enable_sealed_metadata: bool, + unwrapped_metadata_is_private: bool, + minter_may_update_metadata: bool, + owner_may_update_metadata: bool, + enable_burn: bool, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + + let env = mock_env(); + let init_config: InstantiateConfig = from_binary(&Binary::from( + format!( + "{{\"public_token_supply\":{}, + \"public_owner\":{}, + \"enable_sealed_metadata\":{}, + \"unwrapped_metadata_is_private\":{}, + \"minter_may_update_metadata\":{}, + \"owner_may_update_metadata\":{}, + \"enable_burn\":{}}}", + public_token_supply, + public_owner, + enable_sealed_metadata, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + enable_burn, + ) + .as_bytes(), + )) + .unwrap(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: Some(init_config), + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(_response) => panic!("Expected error, but had Ok response"), + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected error result {:?}", err), + }, + } + } + + // test ContractInfo query + #[test] + fn test_query_contract_info() { + let (init_result, deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let query_msg = QueryMsg::ContractInfo {}; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ContractInfo { name, symbol } => { + assert_eq!(name, "sec721".to_string()); + assert_eq!(symbol, "S721".to_string()); + } + _ => panic!("unexpected"), + } + } + + // test ContractConfig query + #[test] + fn test_query_contract_config() { + let (init_result, deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let query_msg = QueryMsg::ContractConfig {}; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ContractConfig { + token_supply_is_public, + owner_is_public, + sealed_metadata_is_enabled, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + burn_is_enabled, + implements_non_transferable_tokens, + implements_token_subtype, + } => { + assert!(!token_supply_is_public); + assert!(owner_is_public); + assert!(sealed_metadata_is_enabled); + assert!(!unwrapped_metadata_is_private); + assert!(minter_may_update_metadata); + assert!(!owner_may_update_metadata); + assert!(burn_is_enabled); + assert!(implements_non_transferable_tokens); + assert!(implements_token_subtype); + } + _ => panic!("unexpected"), + } + } + + // test minters query + #[test] + fn test_query_minters() { + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + + let minters = vec![ + alice.clone(), + bob.clone(), + charlie.clone(), + bob.clone(), + alice.clone(), + ]; + let execute_msg = ExecuteMsg::SetMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::Minters {}; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::Minters { minters } => { + assert_eq!(minters.len(), 3); + assert!(minters.contains(&Addr::unchecked(alice))); + assert!(minters.contains(&Addr::unchecked(bob))); + assert!(minters.contains(&Addr::unchecked(charlie))); + } + _ => panic!("unexpected"), + } + } + + // test NumTokens query + #[test] + fn test_query_num_tokens() { + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Public 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Public 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + + // test non-minter attempt + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + + let minters = vec![alice.clone(), bob.clone(), charlie, bob, alice.clone()]; + let execute_msg = ExecuteMsg::SetMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::NumTokens { viewer: None }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("The token supply of this contract is private")); + + // test minter with bad viewing key + let viewer = ViewerInfo { + address: alice.clone(), + viewing_key: "key".to_string(), + }; + let query_msg = QueryMsg::NumTokens { + viewer: Some(viewer.clone()), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key for this address or viewing key not set")); + + // test valid minter, valid key + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::NumTokens { + viewer: Some(viewer), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 2); + } + _ => panic!("unexpected"), + } + + // test token supply public + let (init_result, mut deps) = + init_helper_with_config(true, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Public 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Public 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let viewer = ViewerInfo { + address: alice, + viewing_key: "key".to_string(), + }; + let query_msg = QueryMsg::NumTokens { + viewer: Some(viewer), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 2); + } + _ => panic!("unexpected"), + } + } + + // test AllTokens query + #[test] + fn test_query_all_tokens() { + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Public 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Public 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My3".to_string()), + description: Some("Public 3".to_string()), + image: Some("URI 3".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test non-minter attempt + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + + let minters = vec![alice.clone(), bob.clone(), charlie, bob, alice.clone()]; + let execute_msg = ExecuteMsg::SetMinters { + minters, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::AllTokens { + viewer: None, + start_after: None, + limit: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("The token supply of this contract is private")); + + // test minter with bad viewing key + let viewer = ViewerInfo { + address: alice.clone(), + viewing_key: "key".to_string(), + }; + let query_msg = QueryMsg::AllTokens { + viewer: Some(viewer.clone()), + start_after: None, + limit: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key for this address or viewing key not set")); + + // test valid minter, valid key only return first two + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::AllTokens { + viewer: Some(viewer), + start_after: None, + limit: Some(2), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT1".to_string(), "NFT2".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test token supply public, with pagination starting after NFT2 + let (init_result, mut deps) = + init_helper_with_config(true, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My1".to_string()), + description: Some("Public 1".to_string()), + image: Some("URI 1".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My2".to_string()), + description: Some("Public 2".to_string()), + image: Some("URI 2".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My3".to_string()), + description: Some("Public 3".to_string()), + image: Some("URI 3".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My5".to_string()), + description: Some("Public 5".to_string()), + image: Some("URI 5".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("alice".to_string()), + public_metadata: Some(Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("My4".to_string()), + description: Some("Public 4".to_string()), + image: Some("URI 4".to_string()), + ..Extension::default() + }), + }), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let viewer = ViewerInfo { + address: alice, + viewing_key: "key".to_string(), + }; + let query_msg = QueryMsg::AllTokens { + viewer: Some(viewer.clone()), + start_after: Some("NFT2".to_string()), + limit: Some(10), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT3".to_string(), "NFT5".to_string(), "NFT4".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test start after token not found + let query_msg = QueryMsg::AllTokens { + viewer: Some(viewer.clone()), + start_after: Some("NFT21".to_string()), + limit: Some(10), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT21 not found")); + + // test burned token does not show + let execute_msg = ExecuteMsg::BurnNft { + token_id: "NFT3".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::AllTokens { + viewer: Some(viewer), + start_after: None, + limit: Some(10), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec![ + "NFT1".to_string(), + "NFT2".to_string(), + "NFT5".to_string(), + "NFT4".to_string(), + ]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + } + + // test NftDossier query + #[test] + fn test_query_nft_dossier() { + let (init_result, deps) = + init_helper_with_config(true, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to perform this action on token NFT1")); + + let public_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PubDesc1".to_string()), + image: Some("PubUri1".to_string()), + ..Extension::default() + }), + }; + let private_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("PrivName1".to_string()), + description: Some("PrivDesc1".to_string()), + image: Some("PrivUri1".to_string()), + ..Extension::default() + }), + }; + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta.clone()), + private_metadata: Some(private_meta.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test viewer not given, contract has public ownership + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 10, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice.clone()))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert!(private_metadata.is_none()); + assert_eq!( + display_private_metadata_error, + Some("You are not authorized to perform this action on token NFT1".to_string()) + ); + assert!(owner_is_public); + assert!(transferable); + assert!(!unwrapped); + assert_eq!(public_ownership_expiration, Some(Expiration::Never)); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + assert!(token_approvals.is_none()); + assert!(inventory_approvals.is_none()); + } + _ => panic!("unexpected"), + } + + // test viewer not given, contract has private ownership, but token ownership + // and private metadata was made public + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta.clone()), + private_metadata: Some(private_meta.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice.clone()))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert_eq!(private_metadata, Some(private_meta.clone())); + assert!(display_private_metadata_error.is_none()); + assert!(owner_is_public); + assert!(transferable); + assert!(unwrapped); + assert_eq!(public_ownership_expiration, Some(Expiration::AtHeight(5))); + assert!(private_metadata_is_public); + assert_eq!( + private_metadata_is_public_expiration, + Some(Expiration::AtHeight(5)) + ); + assert!(token_approvals.is_none()); + assert!(inventory_approvals.is_none()); + } + _ => panic!("unexpected"), + } + + // test no viewer given, ownership and private metadata made public at the + // inventory level + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: Some(Expiration::AtHeight(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtTime(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 1, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice.clone()))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert_eq!(private_metadata, Some(private_meta.clone())); + assert!(transferable); + assert!(unwrapped); + assert!(display_private_metadata_error.is_none()); + assert!(owner_is_public); + assert_eq!(public_ownership_expiration, Some(Expiration::AtHeight(5))); + assert!(private_metadata_is_public); + assert_eq!( + private_metadata_is_public_expiration, + Some(Expiration::AtTime(1000)) + ); + assert!(token_approvals.is_none()); + assert!(inventory_approvals.is_none()); + } + _ => panic!("unexpected"), + } + + // test owner is the viewer including expired + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let viewer = ViewerInfo { + address: alice.clone(), + viewing_key: "key".to_string(), + }; + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: Some(Expiration::AtHeight(10)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let bob_tok_app = Snip721Approval { + address: Addr::unchecked(bob.clone()), + view_owner_expiration: None, + view_private_metadata_expiration: Some(Expiration::Never), + transfer_expiration: None, + }; + let char_tok_app = Snip721Approval { + address: Addr::unchecked(charlie.clone()), + view_owner_expiration: Some(Expiration::AtHeight(5)), + view_private_metadata_expiration: None, + transfer_expiration: None, + }; + let bob_all_app = Snip721Approval { + address: Addr::unchecked(bob.clone()), + view_owner_expiration: Some(Expiration::Never), + view_private_metadata_expiration: None, + transfer_expiration: Some(Expiration::Never), + }; + let char_all_app = Snip721Approval { + address: Addr::unchecked(charlie.clone()), + view_owner_expiration: None, + view_private_metadata_expiration: None, + transfer_expiration: Some(Expiration::AtHeight(5)), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: Some(viewer.clone()), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 10000, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice.clone()))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert_eq!(private_metadata, Some(private_meta.clone())); + assert!(transferable); + assert!(unwrapped); + assert!(display_private_metadata_error.is_none()); + assert!(!owner_is_public); + assert!(public_ownership_expiration.is_none()); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + let token_approvals = token_approvals.unwrap(); + assert_eq!(token_approvals.len(), 2); + assert!(token_approvals.contains(&bob_tok_app)); + assert!(token_approvals.contains(&char_tok_app)); + let inventory_approvals = inventory_approvals.unwrap(); + assert_eq!(inventory_approvals.len(), 2); + assert!(inventory_approvals.contains(&bob_all_app)); + assert!(inventory_approvals.contains(&char_all_app)); + } + _ => panic!("unexpected"), + } + // test owner is the viewer, filtering expired + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: Some(viewer), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice.clone()))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert_eq!(private_metadata, Some(private_meta.clone())); + assert!(transferable); + assert!(unwrapped); + assert!(display_private_metadata_error.is_none()); + assert!(!owner_is_public); + assert!(public_ownership_expiration.is_none()); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + let token_approvals = token_approvals.unwrap(); + assert_eq!(token_approvals.len(), 1); + assert!(token_approvals.contains(&bob_tok_app)); + assert!(!token_approvals.contains(&char_tok_app)); + let inventory_approvals = inventory_approvals.unwrap(); + assert_eq!(inventory_approvals.len(), 1); + assert!(inventory_approvals.contains(&bob_all_app)); + assert!(!inventory_approvals.contains(&char_all_app)); + } + _ => panic!("unexpected"), + } + + // test bad viewing key + let viewer = ViewerInfo { + address: alice.clone(), + viewing_key: "ky".to_string(), + }; + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: Some(viewer), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key for this address or viewing key not set")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, true, true, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta.clone()), + private_metadata: Some(private_meta), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(5)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob, + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test owner is the viewer, but token is sealed + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: "key".to_string(), + }), + include_expired: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 10, + time: Timestamp::from_seconds(100), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert_eq!(owner, Some(Addr::unchecked(alice))); + assert_eq!(public_metadata, Some(public_meta.clone())); + assert!(private_metadata.is_none()); + assert!(transferable); + assert!(!unwrapped); + assert_eq!(display_private_metadata_error, Some("Sealed metadata of token NFT1 must be unwrapped by calling Reveal before it can be viewed".to_string())); + assert!(!owner_is_public); + assert!(public_ownership_expiration.is_none()); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + let token_approvals = token_approvals.unwrap(); + assert_eq!(token_approvals.len(), 1); + assert!(token_approvals.contains(&bob_tok_app)); + assert!(!token_approvals.contains(&char_tok_app)); + let inventory_approvals = inventory_approvals.unwrap(); + assert_eq!(inventory_approvals.len(), 1); + assert!(inventory_approvals.contains(&bob_all_app)); + assert!(!inventory_approvals.contains(&char_all_app)); + } + _ => panic!("unexpected"), + } + let execute_msg = ExecuteMsg::Reveal { + token_id: "NFT1".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test expired view private meta approval + let query_msg = QueryMsg::NftDossier { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: charlie, + viewing_key: "ckey".to_string(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { + owner, + public_metadata, + private_metadata, + royalty_info: _, + mint_run_info: _, + transferable, + unwrapped, + display_private_metadata_error, + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + inventory_approvals, + } => { + assert!(owner.is_none()); + assert_eq!(public_metadata, Some(public_meta)); + assert!(private_metadata.is_none()); + assert!(transferable); + assert!(unwrapped); + assert_eq!( + display_private_metadata_error, + Some("Access to token NFT1 has expired".to_string()) + ); + assert!(!owner_is_public); + assert!(public_ownership_expiration.is_none()); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + assert!(token_approvals.is_none()); + assert!(inventory_approvals.is_none()); + } + _ => panic!("unexpected"), + } + } + + // test Tokens query + #[test] + fn test_query_tokens() { + let (init_result, mut deps) = + init_helper_with_config(false, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT7".to_string()), + owner: Some("bob".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT8".to_string()), + owner: Some("charlie".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test contract has public ownership + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec![ + "NFT1".to_string(), + "NFT2".to_string(), + "NFT3".to_string(), + "NFT4".to_string(), + "NFT5".to_string(), + "NFT6".to_string(), + ]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test a public inventory but not found + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: Some("NFT10".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT10 is not in the specified inventory")); + + // test not in a public inventory + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: Some("NFT7".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT7 is not in the specified inventory")); + + // test limit 0 + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: None, + limit: Some(0), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + assert!(tokens.is_empty()); + } + _ => panic!("unexpected"), + } + + // test limit 1, paginated + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: Some("NFT3".to_string()), + limit: Some(1), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT4".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test no key provided should only see public tokens + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: None, + limit: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT1".to_string(), "NFT3".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test viewer with a a token permission sees that one and the public ones + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: Some(bob.clone()), + viewing_key: Some("bkey".to_string()), + start_after: None, + limit: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT1".to_string(), "NFT3".to_string(), "NFT5".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test paginating with the owner querying + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: Some("akey".to_string()), + start_after: None, + limit: Some(3), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT1".to_string(), "NFT2".to_string(), "NFT3".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: Some("akey".to_string()), + start_after: Some("NFT34".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT34 is not in the specified inventory")); + + // test setting all tokens public + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: None, + start_after: None, + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec![ + "NFT1".to_string(), + "NFT2".to_string(), + "NFT3".to_string(), + "NFT4".to_string(), + "NFT5".to_string(), + "NFT6".to_string(), + ]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // contract with private ownership + let (init_result, mut deps) = init_helper_default(); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let charlie = "charlie".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); // test burn when status prevents it + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT3".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT4".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT5".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT6".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT7".to_string()), + owner: Some("bob".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT8".to_string()), + owner: Some("charlie".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT9".to_string()), + owner: Some("charlie".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT8".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: alice.clone(), + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some("NFT5".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test a start after that is not in the inventory, but the viewer has permission on that token + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: Some(bob.clone()), + viewing_key: Some("bkey".to_string()), + start_after: Some("NFT8".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT8 is not in the specified inventory")); + + // test a start after that is not in the inventory, but viewer has token permission to view owner + let query_msg = QueryMsg::Tokens { + owner: charlie.clone(), + viewer: Some(alice.clone()), + viewing_key: Some("akey".to_string()), + start_after: Some("NFT7".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to perform this action on token NFT7")); + + // test viewer has permission on start after token (but no other) does not error + let query_msg = QueryMsg::Tokens { + owner: charlie.clone(), + viewer: Some(bob), + viewing_key: Some("bkey".to_string()), + start_after: Some("NFT8".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + assert!(tokens.is_empty()); + } + _ => panic!("unexpected"), + } + + // test viewer has operator permission on start after token + let query_msg = QueryMsg::Tokens { + owner: charlie.clone(), + viewer: Some(alice.clone()), + viewing_key: Some("akey".to_string()), + start_after: Some("NFT8".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenList { tokens } => { + let expected = vec!["NFT9".to_string()]; + assert_eq!(tokens, expected); + } + _ => panic!("unexpected"), + } + + // test a start after that viewer does not have permission on, although he does have permission + // on other tokens in the inventory + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: Some(charlie.clone()), + viewing_key: Some("ckey".to_string()), + start_after: Some("NFT3".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to perform this action on token NFT3")); + + // test a bad viewing key + let query_msg = QueryMsg::Tokens { + owner: alice.clone(), + viewer: None, + viewing_key: Some("ckey".to_string()), + start_after: Some("NFT3".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Wrong viewing key for this address or viewing key not set")); + + // test token not found with private supply and private ownership + let query_msg = QueryMsg::Tokens { + owner: alice, + viewer: Some(charlie), + viewing_key: Some("ckey".to_string()), + start_after: Some("NFT34".to_string()), + limit: Some(30), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to perform this action on token NFT34")); + } + + // test IsUnwrapped query + #[test] + fn test_is_unwrapped() { + let (init_result, deps) = + init_helper_with_config(true, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public and sealed meta is disabled + let query_msg = QueryMsg::IsUnwrapped { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, deps) = + init_helper_with_config(false, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private and sealed meta is disabled + let query_msg = QueryMsg::IsUnwrapped { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsUnwrapped { token_is_unwrapped } => { + assert!(token_is_unwrapped); + } + _ => panic!("unexpected"), + } + + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is private and sealed meta is enabled + let query_msg = QueryMsg::IsUnwrapped { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsUnwrapped { token_is_unwrapped } => { + assert!(!token_is_unwrapped); + } + _ => panic!("unexpected"), + } + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // sanity check, token sealed + let query_msg = QueryMsg::IsUnwrapped { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsUnwrapped { token_is_unwrapped } => { + assert!(!token_is_unwrapped); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::Reveal { + token_id: "NFT1".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // sanity check, token unwrapped + let query_msg = QueryMsg::IsUnwrapped { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::IsUnwrapped { token_is_unwrapped } => { + assert!(token_is_unwrapped); + } + _ => panic!("unexpected"), + } + } + + // test OwnerOf query + #[test] + fn test_owner_of() { + let (init_result, mut deps) = + init_helper_with_config(false, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtTime(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob, + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test no viewer given, contract has public ownership + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::OwnerOf { owner, approvals } => { + assert_eq!(owner, Addr::unchecked(alice)); + assert!(approvals.is_empty()); + } + _ => panic!("unexpected"), + } + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some("alice".to_string()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(100)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test viewer with no approvals, but token has public ownership + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: charlie.clone(), + viewing_key: "ckey".to_string(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::OwnerOf { owner, approvals } => { + assert_eq!(owner, Addr::unchecked(alice.clone())); + assert!(approvals.is_empty()); + } + _ => panic!("unexpected"), + } + + // test viewer with no approval, but owner has made all his token ownership public + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: charlie.clone(), + viewing_key: "ckey".to_string(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::OwnerOf { owner, approvals } => { + assert_eq!(owner, Addr::unchecked(alice.clone())); + assert!(approvals.is_empty()); + } + _ => panic!("unexpected"), + } + + // test not permitted to view owner + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::None), + view_private_metadata: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: charlie.clone(), + viewing_key: "ckey".to_string(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to view the owner of token NFT1")); + + // test owner can see approvals including expired + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(1000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let bob_approv = Cw721Approval { + spender: Addr::unchecked(bob), + expires: Expiration::AtHeight(100), + }; + let char_approv = Cw721Approval { + spender: Addr::unchecked(charlie), + expires: Expiration::AtHeight(1000), + }; + + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: "akey".to_string(), + }), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::OwnerOf { owner, approvals } => { + assert_eq!(owner, Addr::unchecked(alice.clone())); + assert_eq!(approvals.len(), 2); + assert_eq!(approvals, vec![bob_approv, char_approv.clone()]) + } + _ => panic!("unexpected"), + } + + // test excluding expired + let query_msg = QueryMsg::OwnerOf { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: "akey".to_string(), + }), + include_expired: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::OwnerOf { owner, approvals } => { + assert_eq!(owner, Addr::unchecked(alice)); + assert_eq!(approvals, vec![char_approv]) + } + _ => panic!("unexpected"), + } + } + + // test NftInfo query + #[test] + fn test_nft_info() { + let (init_result, deps) = + init_helper_with_config(true, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let query_msg = QueryMsg::NftInfo { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test token not found when supply is public + let query_msg = QueryMsg::NftInfo { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftInfo { + token_uri, + extension, + } => { + assert!(token_uri.is_none()); + assert!(extension.is_none()); + } + _ => panic!("unexpected"), + } + let alice = "alice".to_string(); + let public_meta = Metadata { + token_uri: Some("uri".to_string()), + extension: None, + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice), + public_metadata: Some(public_meta.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // sanity check + let query_msg = QueryMsg::NftInfo { + token_id: "NFT1".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftInfo { + token_uri, + extension, + } => { + assert_eq!(token_uri, public_meta.token_uri); + assert_eq!(extension, public_meta.extension); + } + _ => panic!("unexpected"), + } + } + + // test AllNftInfo query + #[test] + fn test_all_nft_info() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let meta_for_fail = Metadata { + token_uri: Some("uri".to_string()), + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PubDesc1".to_string()), + image: Some("PubUri1".to_string()), + ..Extension::default() + }), + }; + + let public_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PubDesc1".to_string()), + image: Some("PubUri1".to_string()), + ..Extension::default() + }), + }; + + // test unable to have both token_uri and extension in the metadata + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFTfail".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(meta_for_fail), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Metadata can not have BOTH token_uri AND extension")); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta.clone()), + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test don't have permission to view owner, but should still be able to see + // public metadata + let query_msg = QueryMsg::AllNftInfo { + token_id: "NFT1".to_string(), + viewer: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::AllNftInfo { access, info } => { + assert!(access.owner.is_none()); + assert!(access.approvals.is_empty()); + assert_eq!(info, Some(public_meta)); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob, + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test owner viewing all nft info, the is no public metadata + let query_msg = QueryMsg::AllNftInfo { + token_id: "NFT2".to_string(), + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: "akey".to_string(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::AllNftInfo { access, info } => { + assert_eq!(access.owner, Some(Addr::unchecked(alice))); + assert_eq!(access.approvals.len(), 1); + assert!(info.is_none()); + } + _ => panic!("unexpected"), + } + } + + // test PrivateMetadata query + #[test] + fn test_private_metadata() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let private_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PrivDesc1".to_string()), + image: Some("PrivUri1".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice), + public_metadata: None, + private_metadata: Some(private_meta.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test global approval on token + let query_msg = QueryMsg::PrivateMetadata { + token_id: "NFT1".to_string(), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::PrivateMetadata { + token_uri, + extension, + } => { + assert_eq!(token_uri, private_meta.token_uri); + assert_eq!(extension, private_meta.extension); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test global approval on all tokens + let query_msg = QueryMsg::PrivateMetadata { + token_id: "NFT1".to_string(), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::PrivateMetadata { + token_uri, + extension, + } => { + assert_eq!(token_uri, private_meta.token_uri); + assert_eq!(extension, private_meta.extension); + } + _ => panic!("unexpected"), + } + + let (init_result, mut deps) = + init_helper_with_config(false, false, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "bkey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + let private_meta = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PrivDesc1".to_string()), + image: Some("PrivUri1".to_string()), + ..Extension::default() + }), + }; + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: Some(private_meta), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test trying to view sealed metadata + let query_msg = QueryMsg::PrivateMetadata { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: "akey".to_string(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains( + "Sealed metadata must be unwrapped by calling Reveal before it can be viewed" + )); + let execute_msg = ExecuteMsg::Reveal { + token_id: "NFT1".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test owner viewing empty metadata after the private got unwrapped to public + let query_msg = QueryMsg::PrivateMetadata { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: alice, + viewing_key: "akey".to_string(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::PrivateMetadata { + token_uri, + extension, + } => { + assert!(token_uri.is_none()); + assert!(extension.is_none()); + } + _ => panic!("unexpected"), + } + + // test viewer not permitted + let query_msg = QueryMsg::PrivateMetadata { + token_id: "NFT1".to_string(), + viewer: Some(ViewerInfo { + address: bob, + viewing_key: "bkey".to_string(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to perform this action on token NFT1")); + } + + // test ApprovedForAll query + #[test] + fn test_approved_for_all() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::ApproveAll { + operator: bob.clone(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::ApproveAll { + operator: charlie.clone(), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test no viewing key supplied + let query_msg = QueryMsg::ApprovedForAll { + owner: alice.clone(), + viewing_key: None, + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ApprovedForAll { operators } => { + assert!(operators.is_empty()); + } + _ => panic!("unexpected"), + } + + let bob_approv = Cw721Approval { + spender: Addr::unchecked(bob), + expires: Expiration::Never, + }; + let char_approv = Cw721Approval { + spender: Addr::unchecked(charlie), + expires: Expiration::Never, + }; + + // sanity check + let query_msg = QueryMsg::ApprovedForAll { + owner: alice, + viewing_key: Some("akey".to_string()), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::ApprovedForAll { operators } => { + assert_eq!(operators, vec![bob_approv, char_approv]); + } + _ => panic!("unexpected"), + } + } + + // test TokenApprovals query + #[test] + fn test_token_approvals() { + let (init_result, deps) = + init_helper_with_config(true, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + + // test token not found when supply is public + let query_msg = QueryMsg::TokenApprovals { + token_id: "NFT1".to_string(), + viewing_key: "akey".to_string(), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT1 not found")); + + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test token not found when supply is private + let query_msg = QueryMsg::TokenApprovals { + token_id: "NFT1".to_string(), + viewing_key: "akey".to_string(), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("You are not authorized to view approvals for token NFT1")); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: Some(alice), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(2000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let bob_approv = Snip721Approval { + address: Addr::unchecked(bob), + view_owner_expiration: None, + view_private_metadata_expiration: Some(Expiration::Never), + transfer_expiration: None, + }; + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + expires: Some(Expiration::AtHeight(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test public ownership when contract has public ownership + // and private meta is public on the token + let query_msg = QueryMsg::TokenApprovals { + token_id: "NFT1".to_string(), + viewing_key: "akey".to_string(), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + } => { + assert!(owner_is_public); + assert_eq!(public_ownership_expiration, Some(Expiration::Never)); + assert!(private_metadata_is_public); + assert_eq!( + private_metadata_is_public_expiration, + Some(Expiration::AtHeight(1000000)) + ); + assert_eq!(token_approvals, vec![bob_approv.clone()]); + } + _ => panic!("unexpected"), + } + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test token has public ownership + // and private meta is public for all of alice's tokens + let query_msg = QueryMsg::TokenApprovals { + token_id: "NFT1".to_string(), + viewing_key: "akey".to_string(), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + } => { + assert!(owner_is_public); + assert_eq!( + public_ownership_expiration, + Some(Expiration::AtHeight(1000000)) + ); + assert!(private_metadata_is_public); + assert_eq!( + private_metadata_is_public_expiration, + Some(Expiration::AtHeight(1000000)) + ); + assert_eq!(token_approvals, vec![bob_approv.clone()]); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::None), + expires: Some(Expiration::AtHeight(2000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + // test all of alice's tokens have public ownership + let query_msg = QueryMsg::TokenApprovals { + token_id: "NFT1".to_string(), + viewing_key: "akey".to_string(), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TokenApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + token_approvals, + } => { + assert!(owner_is_public); + assert_eq!( + public_ownership_expiration, + Some(Expiration::AtHeight(2000000)) + ); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + assert_eq!(token_approvals, vec![bob_approv]); + } + _ => panic!("unexpected"), + } + } + + // test InventoryApprovals query + #[test] + fn test_inventory_approvals() { + let (init_result, mut deps) = + init_helper_with_config(false, true, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: None, + view_private_metadata: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test public ownership when contract has public ownership + // and private metadata is public for all tokens + let query_msg = QueryMsg::InventoryApprovals { + address: alice.clone(), + viewing_key: "akey".to_string(), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::InventoryApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + inventory_approvals, + } => { + assert!(owner_is_public); + assert_eq!(public_ownership_expiration, Some(Expiration::Never)); + assert!(private_metadata_is_public); + assert_eq!( + private_metadata_is_public_expiration, + Some(Expiration::AtHeight(1000000)) + ); + assert!(inventory_approvals.is_empty()); + } + _ => panic!("unexpected"), + } + + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("NFT1".to_string()), + view_owner: None, + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: Some(Expiration::AtHeight(2000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let bob_approv = Snip721Approval { + address: Addr::unchecked(bob), + view_owner_expiration: None, + view_private_metadata_expiration: None, + transfer_expiration: Some(Expiration::AtHeight(2000000)), + }; + + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: Some(Expiration::AtHeight(1000000)), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // test owner makes ownership public for all tokens + let query_msg = QueryMsg::InventoryApprovals { + address: alice, + viewing_key: "akey".to_string(), + include_expired: Some(true), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 500, + time: Timestamp::from_seconds(1000000), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::InventoryApprovals { + owner_is_public, + public_ownership_expiration, + private_metadata_is_public, + private_metadata_is_public_expiration, + inventory_approvals, + } => { + assert!(owner_is_public); + assert_eq!( + public_ownership_expiration, + Some(Expiration::AtHeight(1000000)) + ); + assert!(!private_metadata_is_public); + assert!(private_metadata_is_public_expiration.is_none()); + assert_eq!(inventory_approvals, vec![bob_approv]); + } + _ => panic!("unexpected"), + } + } + + // test VerifyTransferApproval query + #[test] + fn test_verify_transfer_approval() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let bob = "bob".to_string(); + let charlie = "charlie".to_string(); + let david = "david".to_string(); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "ckey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + + let nft1 = "NFT1".to_string(); + let nft2 = "NFT2".to_string(); + let nft3 = "NFT3".to_string(); + let nft4 = "NFT4".to_string(); + let nft5 = "NFT5".to_string(); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some(nft1.clone()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some(nft2.clone()), + owner: Some(alice), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some(nft3.clone()), + owner: Some(bob), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some(nft4.clone()), + owner: Some(charlie.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some(nft5.clone()), + owner: Some(david), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: None, + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::All), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some(nft3.clone()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + // test that charlie can transfer nft1 and 2 with operator approval, + // nft3 with token approval, and nft4 because he owns it + let query_msg = QueryMsg::VerifyTransferApproval { + token_ids: vec![nft1.clone(), nft2.clone(), nft3.clone(), nft4.clone()], + address: charlie.clone(), + viewing_key: "ckey".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::VerifyTransferApproval { + approved_for_all, + first_unapproved_token, + } => { + assert!(approved_for_all); + assert!(first_unapproved_token.is_none()); + } + _ => panic!("unexpected"), + } + + // test an unknown token id + let query_msg = QueryMsg::VerifyTransferApproval { + token_ids: vec![ + nft1.clone(), + nft2.clone(), + "NFT10".to_string(), + nft3.clone(), + nft4.clone(), + ], + address: charlie.clone(), + viewing_key: "ckey".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::VerifyTransferApproval { + approved_for_all, + first_unapproved_token, + } => { + assert!(!approved_for_all); + assert_eq!(first_unapproved_token, Some("NFT10".to_string())); + } + _ => panic!("unexpected"), + } + + // test not having approval on NFT5 + let query_msg = QueryMsg::VerifyTransferApproval { + token_ids: vec![nft1, nft2, nft3, nft4, nft5], + address: charlie, + viewing_key: "ckey".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::VerifyTransferApproval { + approved_for_all, + first_unapproved_token, + } => { + assert!(!approved_for_all); + assert_eq!(first_unapproved_token, Some("NFT5".to_string())); + } + _ => panic!("unexpected"), + } + } + + // test TransactionHistory query + #[test] + fn test_transaction_history() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let admin = "admin".to_string(); + let alice = "alice".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: "akey".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test no txs yet + let query_msg = QueryMsg::TransactionHistory { + address: admin.clone(), + viewing_key: "key".to_string(), + page: None, + page_size: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert!(txs.is_empty()); + assert_eq!(total, 0); + } + _ => panic!("unexpected"), + } + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT1".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("NFT2".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: Some("Mint 2".to_string()), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::TransferNft { + token_id: "NFT1".to_string(), + recipient: alice.clone(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let execute_msg = ExecuteMsg::BurnNft { + token_id: "NFT2".to_string(), + memo: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let mint1 = Tx { + tx_id: 0, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT1".to_string(), + memo: None, + action: TxAction::Mint { + minter: Addr::unchecked(admin.clone()), + recipient: Addr::unchecked(admin.clone()), + }, + }; + let mint2 = Tx { + tx_id: 1, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT2".to_string(), + memo: Some("Mint 2".to_string()), + action: TxAction::Mint { + minter: Addr::unchecked(admin.clone()), + recipient: Addr::unchecked(admin.clone()), + }, + }; + let xfer1 = Tx { + tx_id: 2, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT1".to_string(), + memo: None, + action: TxAction::Transfer { + from: Addr::unchecked(admin.clone()), + sender: None, + recipient: Addr::unchecked(alice.clone()), + }, + }; + let burn2 = Tx { + tx_id: 3, + block_height: 12345, + block_time: 1571797419, + token_id: "NFT2".to_string(), + memo: None, + action: TxAction::Burn { + owner: Addr::unchecked(admin.clone()), + burner: None, + }, + }; + + // sanity check for all txs + let query_msg = QueryMsg::TransactionHistory { + address: admin.clone(), + viewing_key: "key".to_string(), + page: None, + page_size: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!( + txs, + vec![burn2.clone(), xfer1.clone(), mint2.clone(), mint1] + ); + assert_eq!(total, 4); + } + _ => panic!("unexpected"), + } + + // test paginating so only see last 2 + let query_msg = QueryMsg::TransactionHistory { + address: admin.clone(), + viewing_key: "key".to_string(), + page: None, + page_size: Some(2), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!(txs, vec![burn2, xfer1.clone()]); + assert_eq!(total, 4); + } + _ => panic!("unexpected"), + } + + // test paginating so only see 3rd one + let query_msg = QueryMsg::TransactionHistory { + address: admin, + viewing_key: "key".to_string(), + page: Some(2), + page_size: Some(1), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!(txs, vec![mint2]); + assert_eq!(total, 4); + } + _ => panic!("unexpected"), + } + + // test tx was logged to all participants + let query_msg = QueryMsg::TransactionHistory { + address: alice, + viewing_key: "akey".to_string(), + page: None, + page_size: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::TransactionHistory { total, txs } => { + assert_eq!(txs, vec![xfer1]); + assert_eq!(total, 1); + } + _ => panic!("unexpected"), + } + } + + // test RegisteredCodeHash query + #[test] + fn test_query_registered_code_hash() { + let (init_result, mut deps) = + init_helper_with_config(false, true, true, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + // test not registered + let query_msg = QueryMsg::RegisteredCodeHash { + contract: "alice".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RegisteredCodeHash { + code_hash, + also_implements_batch_receive_nft, + } => { + assert!(code_hash.is_none()); + assert!(!also_implements_batch_receive_nft) + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "Code Hash".to_string(), + also_implements_batch_receive_nft: None, + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + // sanity check with default for implements BatchReceiveNft + let query_msg = QueryMsg::RegisteredCodeHash { + contract: "alice".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RegisteredCodeHash { + code_hash, + also_implements_batch_receive_nft, + } => { + assert_eq!(code_hash, Some("Code Hash".to_string())); + assert!(!also_implements_batch_receive_nft) + } + _ => panic!("unexpected"), + } + + // sanity check with implementing BatchRegisterReceive + let execute_msg = ExecuteMsg::RegisterReceiveNft { + code_hash: "Code Hash".to_string(), + also_implements_batch_receive_nft: Some(true), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + let query_msg = QueryMsg::RegisteredCodeHash { + contract: "bob".to_string(), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RegisteredCodeHash { + code_hash, + also_implements_batch_receive_nft, + } => { + assert_eq!(code_hash, Some("Code Hash".to_string())); + assert!(also_implements_batch_receive_nft) + } + _ => panic!("unexpected"), + } + } + + // test NumTokensOfOwner query + #[test] + fn test_num_tokens_of_owner() { + let (init_result, mut deps) = + init_helper_with_config(false, true, false, false, false, false, false); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + let alice_key = "akey".to_string(); + let bob = "bob".to_string(); + let bob_key = "bkey".to_string(); + let charlie = "charlie".to_string(); + let charlie_key = "ckey".to_string(); + + let mints = vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // let charlie see the owner of NFT2 until time 55 + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: charlie.clone(), + token_id: Some("NFT2".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + transfer: None, + expires: Some(Expiration::AtTime(55)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test all 3 are public from the config + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: None, + viewing_key: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 2, + time: Timestamp::from_seconds(2), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 3); + } + _ => panic!("unexpected"), + } + + // set ownership to private for alice + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // set charlie's viewing key + let execute_msg = ExecuteMsg::SetViewingKey { + key: charlie_key.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("charlie", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that charlie can only know of NFT2 + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: Some(charlie.clone()), + viewing_key: Some(charlie_key.clone()), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 3, + time: Timestamp::from_seconds(3), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 1); + } + _ => panic!("unexpected"), + } + + // set alice's viewing key + let execute_msg = ExecuteMsg::SetViewingKey { + key: alice_key.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that alice knows of all her tokens + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: None, + viewing_key: Some(alice_key), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 4, + time: Timestamp::from_seconds(4), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 3); + } + _ => panic!("unexpected"), + } + + // make ownership public until time 15 + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + expires: Some(Expiration::AtTime(15)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // set bob's viewing key + let execute_msg = ExecuteMsg::SetViewingKey { + key: bob_key.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that bob can use the global approval to know of all 3 + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: Some(bob.clone()), + viewing_key: Some(bob_key.clone()), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 10, + time: Timestamp::from_seconds(10), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 3); + } + _ => panic!("unexpected"), + } + + // let bob see all alice ownership until time 25 + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: None, + view_owner: Some(AccessLevel::All), + view_private_metadata: None, + transfer: None, + expires: Some(Expiration::AtTime(25)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that the global approval has expired + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: None, + viewing_key: None, + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 20, + time: Timestamp::from_seconds(20), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 0); + } + _ => panic!("unexpected"), + } + + // test that bob can use his approval to know of all 3 now that global has expired + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: Some(bob.clone()), + viewing_key: Some(bob_key.clone()), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 20, + time: Timestamp::from_seconds(20), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 3); + } + _ => panic!("unexpected"), + } + + // make ownership public for NFT2 until time 25 + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT2".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: Some(Expiration::AtTime(25)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // make ownership public for NFT1 until time 50 + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT1".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: Some(Expiration::AtTime(50)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // make ownership public for NFT3 until time 60 + let execute_msg = ExecuteMsg::SetGlobalApproval { + token_id: Some("NFT3".to_string()), + view_owner: Some(AccessLevel::ApproveToken), + view_private_metadata: None, + expires: Some(Expiration::AtTime(60)), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that charlie knows of all of them by using the two global approvals and his own + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: Some(charlie.clone()), + viewing_key: Some(charlie_key.clone()), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 30, + time: Timestamp::from_seconds(30), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 3); + } + _ => panic!("unexpected"), + } + + // test that bob knows of NFT1 and NFT3 by using the two global approvals + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice.clone(), + viewer: Some(bob), + viewing_key: Some(bob_key), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 30, + time: Timestamp::from_seconds(30), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 2); + } + _ => panic!("unexpected"), + } + /* TODO + // set ownership to private for alice again just to change the saved blockinfo + let execute_msg = ExecuteMsg::MakeOwnershipPrivate { padding: None }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + */ + // test that charlie only knows of NFT3 now that the other two approvals expired + let query_msg = QueryMsg::NumTokensOfOwner { + owner: alice, + viewer: Some(charlie), + viewing_key: Some(charlie_key), + }; + let query_result = query( + deps.as_ref(), + Env { + block: BlockInfo { + height: 57, + time: Timestamp::from_seconds(57), + chain_id: "cosmos-testnet-14002".to_string(), + random: None, + }, + transaction: None, + contract: cosmwasm_std::ContractInfo { + address: Addr::unchecked(MOCK_CONTRACT_ADDR), + code_hash: "".to_string(), + }, + }, + query_msg, + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NumTokens { count } => { + assert_eq!(count, 1); + } + _ => panic!("unexpected"), + } + } + + // test BatchNftDossier query + #[test] + fn test_query_batch_nft_dossier() { + let (init_result, mut deps) = + init_helper_with_config(false, false, false, false, true, false, true); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let alice = "alice".to_string(); + + let public_meta1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name1".to_string()), + description: Some("PubDesc1".to_string()), + image: Some("PubUri1".to_string()), + ..Extension::default() + }), + }; + let private_meta1 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("PrivName1".to_string()), + description: Some("PrivDesc1".to_string()), + image: Some("PrivUri1".to_string()), + ..Extension::default() + }), + }; + let public_meta2 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name2".to_string()), + description: Some("PubDesc2".to_string()), + image: Some("PubUri2".to_string()), + ..Extension::default() + }), + }; + let private_meta2 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("PrivName2".to_string()), + description: Some("PrivDesc2".to_string()), + image: Some("PrivUri2".to_string()), + ..Extension::default() + }), + }; + + let public_meta3 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("Name3".to_string()), + description: Some("PubDesc3".to_string()), + image: Some("PubUri3".to_string()), + ..Extension::default() + }), + }; + let private_meta3 = Metadata { + token_uri: None, + extension: Some(Extension { + name: Some("PrivName3".to_string()), + description: Some("PrivDesc3".to_string()), + image: Some("PrivUri3".to_string()), + ..Extension::default() + }), + }; + + let mints = vec![ + Mint { + token_id: Some("NFT1".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta1.clone()), + private_metadata: Some(private_meta1.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT2".to_string()), + owner: Some(alice.clone()), + public_metadata: Some(public_meta2.clone()), + private_metadata: Some(private_meta2.clone()), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + Mint { + token_id: Some("NFT3".to_string()), + owner: Some("bob".to_string()), + public_metadata: Some(public_meta3.clone()), + private_metadata: Some(private_meta3), + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + }, + ]; + + let execute_msg = ExecuteMsg::BatchMintNft { + mints, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test querying all 3 + let alice_key = "akey".to_string(); + let execute_msg = ExecuteMsg::SetViewingKey { + key: alice_key.clone(), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let query_msg = QueryMsg::BatchNftDossier { + token_ids: vec!["NFT1".to_string(), "NFT2".to_string(), "NFT3".to_string()], + viewer: Some(ViewerInfo { + address: alice.clone(), + viewing_key: alice_key, + }), + include_expired: None, + }; + let mint_run_info = MintRunInfo { + collection_creator: Some(Addr::unchecked("instantiator".to_string())), + token_creator: Some(Addr::unchecked("admin".to_string())), + time_of_minting: Some(1571797419), + mint_run: None, + serial_number: None, + quantity_minted_this_run: None, + }; + let expected = vec![ + BatchNftDossierElement { + token_id: "NFT1".to_string(), + owner: Some(Addr::unchecked(alice.clone())), + public_metadata: Some(public_meta1), + private_metadata: Some(private_meta1), + display_private_metadata_error: None, + royalty_info: None, + mint_run_info: Some(mint_run_info.clone()), + transferable: true, + unwrapped: true, + owner_is_public: false, + public_ownership_expiration: None, + private_metadata_is_public: false, + private_metadata_is_public_expiration: None, + token_approvals: Some(Vec::new()), + inventory_approvals: Some(Vec::new()), + }, + BatchNftDossierElement { + token_id: "NFT2".to_string(), + owner: Some(Addr::unchecked(alice)), + public_metadata: Some(public_meta2), + private_metadata: Some(private_meta2), + display_private_metadata_error: None, + royalty_info: None, + mint_run_info: Some(mint_run_info.clone()), + transferable: true, + unwrapped: true, + owner_is_public: false, + public_ownership_expiration: None, + private_metadata_is_public: false, + private_metadata_is_public_expiration: None, + token_approvals: Some(Vec::new()), + inventory_approvals: Some(Vec::new()), + }, + // last one belongs to bob, so you can only see public info + BatchNftDossierElement { + token_id: "NFT3".to_string(), + owner: None, + public_metadata: Some(public_meta3), + private_metadata: None, + display_private_metadata_error: Some( + "You are not authorized to perform this action on token NFT3".to_string(), + ), + royalty_info: None, + mint_run_info: Some(mint_run_info), + transferable: true, + unwrapped: true, + owner_is_public: false, + public_ownership_expiration: None, + private_metadata_is_public: false, + private_metadata_is_public_expiration: None, + token_approvals: None, + inventory_approvals: None, + }, + ]; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::BatchNftDossier { nft_dossiers } => { + assert_eq!(nft_dossiers, expected); + } + _ => panic!("unexpected"), + } + } +} diff --git a/contracts/external/snip721-roles-impl/src/unittest_royalties.rs b/contracts/external/snip721-roles-impl/src/unittest_royalties.rs new file mode 100644 index 0000000..51ae54d --- /dev/null +++ b/contracts/external/snip721-roles-impl/src/unittest_royalties.rs @@ -0,0 +1,958 @@ +#[cfg(test)] +mod tests { + use std::any::Any; + + use cosmwasm_std::testing::*; + use cosmwasm_std::{ + from_binary, to_binary, Addr, Api, Binary, Coin, OwnedDeps, Response, StdError, StdResult, + SubMsg, Uint128, WasmMsg, + }; + + use crate::contract::{execute, instantiate, query}; + use crate::msg::{ + AccessLevel, ContractStatus, ExecuteMsg, InstantiateConfig, InstantiateMsg, + PostInstantiateCallback, QueryAnswer, QueryMsg, ViewerInfo, + }; + use crate::royalties::{DisplayRoyalty, DisplayRoyaltyInfo, Royalty, RoyaltyInfo}; + use crate::state::{load, Config, CONFIG_KEY}; + + // Helper functions + + fn init_helper_royalties( + royalty_info: Option, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info, + config: None, + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + #[allow(clippy::too_many_arguments)] + fn init_helper_royalties_with_config( + royalty_info: Option, + public_token_supply: bool, + public_owner: bool, + enable_sealed_metadata: bool, + unwrapped_metadata_is_private: bool, + minter_may_update_metadata: bool, + owner_may_update_metadata: bool, + enable_burn: bool, + ) -> ( + StdResult, + OwnedDeps, + ) { + let mut deps = mock_dependencies(); + + let env = mock_env(); + let init_config: InstantiateConfig = from_binary(&Binary::from( + format!( + "{{\"public_token_supply\":{}, + \"public_owner\":{}, + \"enable_sealed_metadata\":{}, + \"unwrapped_metadata_is_private\":{}, + \"minter_may_update_metadata\":{}, + \"owner_may_update_metadata\":{}, + \"enable_burn\":{}}}", + public_token_supply, + public_owner, + enable_sealed_metadata, + unwrapped_metadata_is_private, + minter_may_update_metadata, + owner_may_update_metadata, + enable_burn, + ) + .as_bytes(), + )) + .unwrap(); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info, + config: Some(init_config), + post_init_callback: None, + }; + + (instantiate(deps.as_mut(), env, info, init_msg), deps) + } + + fn extract_error_msg(error: StdResult) -> String { + match error { + Ok(_response) => panic!("Expected error, but had Ok response"), + Err(err) => match err { + StdError::GenericErr { msg, .. } => msg, + _ => panic!("Unexpected error result {:?}", err), + }, + } + } + + // Init tests + + #[test] + fn test_init_sanity() { + let royalties = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + Royalty { + recipient: "alice".to_string(), + rate: 10, + }, + Royalty { + recipient: "bob".to_string(), + rate: 5, + }, + ], + }; + + // test default config + let (init_result, mut deps) = init_helper_royalties(Some(royalties.clone())); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(!config.token_supply_is_public); + assert!(!config.owner_is_public); + assert!(!config.sealed_metadata_is_enabled); + assert!(!config.unwrap_to_private); + assert!(config.minter_may_update_metadata); + assert!(!config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + let expected_see = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + DisplayRoyalty { + recipient: Some(Addr::unchecked("alice".to_string())), + rate: 10, + }, + DisplayRoyalty { + recipient: Some(Addr::unchecked("bob".to_string())), + rate: 5, + }, + ], + }; + + let expected_hidden = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + DisplayRoyalty { + recipient: None, + rate: 10, + }, + DisplayRoyalty { + recipient: None, + rate: 5, + }, + ], + }; + + // test viewer not permitted to see default royalty addresses + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_hidden.clone())); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::SetViewingKey { + key: "key".to_string(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + // test viewer is permitted to see default royatly addresses + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: Some(ViewerInfo { + address: "admin".to_string(), + viewing_key: "key".to_string(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_see)); + } + _ => panic!("unexpected"), + } + + // test config specification + let (init_result, deps) = init_helper_royalties_with_config( + Some(royalties), + true, + true, + true, + true, + false, + true, + false, + ); + assert_eq!(init_result.unwrap(), Response::default()); + let config: Config = load(&deps.storage, CONFIG_KEY).unwrap(); + assert_eq!(config.status, ContractStatus::Normal.to_u8()); + assert_eq!(config.mint_cnt, 0); + assert_eq!(config.tx_cnt, 0); + assert_eq!(config.name, "sec721".to_string()); + assert_eq!(config.admin, deps.api.addr_canonicalize("admin").unwrap()); + assert_eq!(config.symbol, "S721".to_string()); + assert!(config.token_supply_is_public); + assert!(config.owner_is_public); + assert!(config.sealed_metadata_is_enabled); + assert!(config.unwrap_to_private); + assert!(!config.minter_may_update_metadata); + assert!(config.owner_may_update_metadata); + assert!(!config.burn_is_enabled); + + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_hidden)); + } + _ => panic!("unexpected"), + } + + // test post init callback + let mut deps = mock_dependencies(); + let env = mock_env(); + // just picking a random short HandleMsg that wouldn't really make sense + let post_init_msg = to_binary(&ExecuteMsg::MakeOwnershipPrivate { padding: None }).unwrap(); + let post_init_send = vec![Coin { + amount: Uint128::new(100), + denom: "uscrt".to_string(), + }]; + let post_init_callback = Some(PostInstantiateCallback { + msg: post_init_msg.clone(), + contract_address: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + send: post_init_send.clone(), + }); + let info = mock_info("instantiator", &[]); + let init_msg = InstantiateMsg { + name: "sec721".to_string(), + symbol: "S721".to_string(), + admin: Some("admin".to_string()), + entropy: "We're going to need a bigger boat".to_string(), + royalty_info: None, + config: None, + post_init_callback, + }; + + let init_response = instantiate(deps.as_mut(), env, info, init_msg).unwrap(); + assert_eq!( + init_response.messages, + vec![SubMsg::new(WasmMsg::Execute { + msg: post_init_msg, + contract_addr: "spawner".to_string(), + code_hash: "spawner hash".to_string(), + funds: post_init_send, + })] + ); + } + + // test setting royalty info + #[test] + fn test_set_royalty_info() { + let (init_result, mut deps) = init_helper_royalties(None); + assert!( + init_result.is_ok(), + "Init failed: {}", + init_result.err().unwrap() + ); + + let royalties = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + Royalty { + recipient: "steven".to_string(), + rate: 10, + }, + Royalty { + recipient: "thomas".to_string(), + rate: 5, + }, + ], + }; + + // test non-minter attempting to set default royalties + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: Some(royalties.clone()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!( + error.contains("Only designated minters can set default royalties for the contract") + ); + + // test royalties more than 100% + let royalty_info_for_failure = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + Royalty { + recipient: "steven".to_string(), + rate: 80, + }, + Royalty { + recipient: "thomas".to_string(), + rate: 20, + }, + Royalty { + recipient: "uriel".to_string(), + rate: 1, + }, + ], + }; + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: Some(royalty_info_for_failure), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("The sum of royalty rates must not exceed 100%")); + + // verify no default royalties are set + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, None); + } + _ => panic!("unexpected"), + } + + let expected_hidden = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + DisplayRoyalty { + recipient: None, + rate: 10, + }, + DisplayRoyalty { + recipient: None, + rate: 5, + }, + ], + }; + + let expected_see = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![ + DisplayRoyalty { + recipient: Some(Addr::unchecked("steven".to_string())), + rate: 10, + }, + DisplayRoyalty { + recipient: Some(Addr::unchecked("thomas".to_string())), + rate: 5, + }, + ], + }; + let admin = "admin".to_string(); + let admin_key = "key".to_string(); + let alice = "alice".to_string(); + let alice_key = "akey".to_string(); + let bob = "bob".to_string(); + let bob_key = "bkey".to_string(); + + // test unknown token error when supply is private but a minter is querying + let execute_msg = ExecuteMsg::SetViewingKey { + key: admin_key.clone(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: alice_key.clone(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + + let execute_msg = ExecuteMsg::SetViewingKey { + key: bob_key.clone(), + padding: None, + }; + let _handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("bob", &[]), + execute_msg, + ); + + // set default royalties sanity check + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: Some(royalties.clone()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_hidden.clone())); + } + _ => panic!("unexpected"), + } + + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: Some(ViewerInfo { + address: admin.clone(), + viewing_key: admin_key.clone(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_see)); + } + _ => panic!("unexpected"), + } + + // test unknown token error during query when supply is private + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("NFT".to_string()), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(expected_hidden)); + } + _ => panic!("unexpected"), + } + + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("NFT".to_string()), + viewer: Some(ViewerInfo { + address: admin.clone(), + viewing_key: admin_key.clone(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT not found")); + + // verify default gets deleted + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + assert!( + query_result.is_ok(), + "query failed: {}", + query_result.err().unwrap() + ); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, None); + } + _ => panic!("unexpected"), + } + + // test unknown token id error when supply is private + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("NFT".to_string()), + royalty_info: Some(royalties.clone()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("A token's RoyaltyInfo may only be set by the token creator when they are also the token owner")); + + let default = RoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![Royalty { + recipient: "default".to_string(), + rate: 10, + }], + }; + let individual = RoyaltyInfo { + decimal_places_in_rates: 3, + royalties: vec![Royalty { + recipient: "individual".to_string(), + rate: 10, + }], + }; + let default_hide = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![DisplayRoyalty { + recipient: None, + rate: 10, + }], + }; + let default_see = DisplayRoyaltyInfo { + decimal_places_in_rates: 2, + royalties: vec![DisplayRoyalty { + recipient: Some(Addr::unchecked("default".to_string())), + rate: 10, + }], + }; + let individual_hide = DisplayRoyaltyInfo { + decimal_places_in_rates: 3, + royalties: vec![DisplayRoyalty { + recipient: None, + rate: 10, + }], + }; + let individual_see = DisplayRoyaltyInfo { + decimal_places_in_rates: 3, + royalties: vec![DisplayRoyalty { + recipient: Some(Addr::unchecked("individual".to_string())), + rate: 10, + }], + }; + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: Some(default), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("default".to_string()), + owner: Some(alice.clone()), + public_metadata: None, + private_metadata: None, + royalty_info: None, + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify it has the default royalties with hidden addresses for bob + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("default".to_string()), + viewer: Some(ViewerInfo { + address: bob.clone(), + viewing_key: bob_key.clone(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(default_hide)); + } + _ => panic!("unexpected"), + } + + // verify it has the default royalties with viewable addresses for alice + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("default".to_string()), + viewer: Some(ViewerInfo { + address: alice, + viewing_key: alice_key, + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(default_see.clone())); + } + _ => panic!("unexpected"), + } + + let execute_msg = ExecuteMsg::MintNft { + token_id: Some("specified".to_string()), + owner: None, + public_metadata: None, + private_metadata: None, + royalty_info: Some(individual.clone()), + serial_number: None, + transferable: None, + memo: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify whitelisting bob for anything but transfer does not reveal addresses + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("specified".to_string()), + view_owner: Some(AccessLevel::All), + view_private_metadata: Some(AccessLevel::ApproveToken), + transfer: None, + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // verify it has the individual royalties with hidden addresses + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("specified".to_string()), + viewer: Some(ViewerInfo { + address: bob.clone(), + viewing_key: bob_key.clone(), + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(individual_hide.clone())); + } + _ => panic!("unexpected"), + } + + // verify nft_dossier also hides addresses + let query_msg = QueryMsg::NftDossier { + token_id: "specified".to_string(), + viewer: Some(ViewerInfo { + address: bob.clone(), + viewing_key: bob_key.clone(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { royalty_info, .. } => { + assert_eq!(royalty_info, Some(individual_hide)); + } + _ => panic!("unexpected"), + } + + // verify that whitelisting bob for transfers reveals the royalty addresses + let execute_msg = ExecuteMsg::SetWhitelistedApproval { + address: bob.clone(), + token_id: Some("specified".to_string()), + view_owner: None, + view_private_metadata: None, + transfer: Some(AccessLevel::ApproveToken), + expires: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + let query_msg = QueryMsg::NftDossier { + token_id: "specified".to_string(), + viewer: Some(ViewerInfo { + address: bob.clone(), + viewing_key: bob_key.clone(), + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { royalty_info, .. } => { + assert_eq!(royalty_info, Some(individual_see)); + } + _ => panic!("unexpected"), + } + + // verify contract default + let query_msg = QueryMsg::RoyaltyInfo { + token_id: None, + viewer: Some(ViewerInfo { + address: admin, + viewing_key: admin_key, + }), + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info } => { + assert_eq!(royalty_info, Some(default_see.clone())); + } + _ => panic!("unexpected"), + } + + // test setting royalties for a token if not owner + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("default".to_string()), + royalty_info: Some(individual.clone()), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("A token's RoyaltyInfo may only be set by the token creator when they are also the token owner")); + + // test trying to set royalties for a token if not the creator + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("default".to_string()), + royalty_info: Some(individual), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("alice", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("A token's RoyaltyInfo may only be set by the token creator when they are also the token owner")); + + // test that deleting individual royalties updates the token to use the default + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("specified".to_string()), + royalty_info: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + let query_msg = QueryMsg::NftDossier { + token_id: "specified".to_string(), + viewer: Some(ViewerInfo { + address: bob, + viewing_key: bob_key, + }), + include_expired: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::NftDossier { royalty_info, .. } => { + assert_eq!(royalty_info, Some(default_see)); + } + _ => panic!("unexpected"), + } + + // delete the default royalties + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: None, + royalty_info: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + + // test that deleting a token's royalties when there is no default, results in no royalties + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("specified".to_string()), + royalty_info: None, + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + assert!(handle_result.is_ok()); + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("specified".to_string()), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let query_answer: QueryAnswer = from_binary(&query_result.unwrap()).unwrap(); + match query_answer { + QueryAnswer::RoyaltyInfo { royalty_info, .. } => { + assert_eq!(royalty_info, None); + } + _ => panic!("unexpected"), + } + + // test unknown token id when supply is public + let (init_result, mut deps) = init_helper_royalties_with_config( + Some(royalties.clone()), + true, + true, + true, + true, + false, + true, + false, + ); + assert_eq!(init_result.unwrap(), Response::default()); + // test unknown token id error when supply is private + let execute_msg = ExecuteMsg::SetRoyaltyInfo { + token_id: Some("NFT".to_string()), + royalty_info: Some(royalties), + padding: None, + }; + let handle_result = execute( + deps.as_mut(), + mock_env(), + mock_info("admin", &[]), + execute_msg, + ); + let error = extract_error_msg(handle_result); + assert!(error.contains("Token ID: NFT not found")); + + let query_msg = QueryMsg::RoyaltyInfo { + token_id: Some("NFT".to_string()), + viewer: None, + }; + let query_result = query(deps.as_ref(), mock_env(), query_msg); + let error = extract_error_msg(query_result); + assert!(error.contains("Token ID: NFT not found")); + } +} diff --git a/contracts/external/snip721-roles-impl/tests/integration.rs b/contracts/external/snip721-roles-impl/tests/integration.rs new file mode 100644 index 0000000..35fb6b8 --- /dev/null +++ b/contracts/external/snip721-roles-impl/tests/integration.rs @@ -0,0 +1,3 @@ +#[test] +#[ignore] +fn empty_test() {} diff --git a/contracts/external/snip721-roles/Cargo.toml b/contracts/external/snip721-roles/Cargo.toml index 784ef36..0a7efab 100644 --- a/contracts/external/snip721-roles/Cargo.toml +++ b/contracts/external/snip721-roles/Cargo.toml @@ -31,7 +31,7 @@ dao-snip721-extensions = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } secret-toolkit ={ workspace = true} -snip721-reference-impl ={ workspace = true } +snip721-roles-impl ={ workspace = true } schemars ={ workspace=true } shade-protocol ={ workspace = true } diff --git a/contracts/external/snip721-roles/src/contract.rs b/contracts/external/snip721-roles/src/contract.rs index 1812757..eafee55 100644 --- a/contracts/external/snip721-roles/src/contract.rs +++ b/contracts/external/snip721-roles/src/contract.rs @@ -1,126 +1,60 @@ -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::snip721::{self, Snip721ExecuteMsg, Snip721QueryAnswer, Snip721QueryMsg}; -use cosmwasm_schema::serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdError, StdResult, SubMsg, SubMsgResult, Uint64, WasmMsg, + from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, + StdError, StdResult, SubMsg, Uint64, }; use cw4::{ Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, TotalWeightResponse, }; -use schemars::JsonSchema; +use dao_snip721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; use secret_cw_controllers::HookItem; -use secret_toolkit::utils::InitCallback; -use serde::{Deserialize, Serialize}; -// use cw721_base::Cw721Contract; -// use snip721_reference_impl::msg::InstantiateMsg as Cw721BaseInstantiateMsg; - -use dao_snip721_extensions::roles::{ExecuteExt, QueryExt}; use shade_protocol::basic_staking::{Auth, AuthPermit}; use shade_protocol::query_auth::helpers::{ authenticate_permit, authenticate_vk, PermitAuthentication, }; use shade_protocol::Contract; +use snip721_roles_impl::msg::{NftInfo, OwnerOf}; +use snip721_roles_impl::{ + msg::InstantiateMsg as Snip721BaseInstantiateMsg, state::Snip721Contract, +}; use std::cmp::Ordering; -// use snip721_reference_impl::msg::{ExecuteMsg as Snip721ExecuteMsg}; -use crate::state::{Config, MembersStore, TotalStore, MEMBERS_PRIMARY, QUERY_AUTH, SNIP721_INFO}; +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::state::{MembersStore, TotalStore, MEMBERS_PRIMARY}; use crate::{error::RolesContractError as ContractError, state::HOOKS}; // Version info for migration -const CONTRACT_NAME: &str = "crates.io:snip721-roles"; +const CONTRACT_NAME: &str = "crates.io:cw721-roles"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // Settings for query pagination const MAX_LIMIT: u32 = 30; const DEFAULT_LIMIT: u32 = 10; -pub const PREFIX_REVOKED_PERMITS: &str = "revoked_permits"; - -#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub enum Snip721ReceiveMsg { - /// ReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver - /// interface. BatchReceiveNft, which is more informative and more efficient, is preferred over - /// ReceiveNft. Please read above regarding why ReceiveNft, which follows CW-721 standard has an - /// inaccurately named `sender` field - ReceiveNft { - /// previous owner of sent token - sender: Addr, - /// token that was sent - token_id: String, - /// optional message to control receiving logic - msg: Option, - }, - /// BatchReceiveNft may be a HandleMsg variant of any contract that wants to implement a receiver - /// interface. BatchReceiveNft, which is more informative and more efficient, is preferred over - /// ReceiveNft. - BatchReceiveNft { - /// address that sent the tokens. There is no ReceiveNft field equivalent to this - sender: Addr, - /// previous owner of sent tokens. This is equivalent to the ReceiveNft `sender` field - from: Addr, - /// tokens that were sent - token_ids: Vec, - /// optional message to control receiving logic - msg: Option, - }, -} - -const SNIP721_INIT_ID: u64 = 0; - -// pub type Cw721Roles<'a> = Cw721Contract<'a, MetadataExt, Empty, ExecuteExt, QueryExt>; +pub type Snip721roles = Snip721Contract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, - msg: InstantiateMsg, + msg: Snip721BaseInstantiateMsg, ) -> Result { - cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_ref()))?; - // Cw721Roles::default().instantiate(deps.branch(), env.clone(), info, msg)?; - - // init snip721 - let init_msg = snip721::Snip721InstantiateMsg { - name: msg.name, - symbol: msg.symbol, - admin: Some(env.contract.address.to_string().clone()), - entropy: msg.entropy, - royalty_info: None, - config: msg.config, - post_init_callback: None, - }; - - let submsg = SubMsg::reply_always( - init_msg.to_cosmos_msg( - Some(info.sender.clone().to_string()), - msg.label.clone(), - msg.code_id, - msg.code_hash.clone(), - None, - )?, - SNIP721_INIT_ID, - ); + Snip721roles::default().instantiate(deps.branch(), env.clone(), info, msg)?; // Initialize total weight to zero TotalStore::save(deps.storage, env.block.height, 0)?; - QUERY_AUTH.save(deps.storage, &msg.query_auth.into_valid(deps.api)?)?; - secret_cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - Ok(Response::new() + Ok(Response::default() .add_attribute("contract_name", CONTRACT_NAME) - .add_attribute("contract_version", CONTRACT_VERSION) - .add_submessage(submsg)) + .add_attribute("contract_version", CONTRACT_VERSION)) } #[cfg_attr(not(feature = "library"), entry_point)] -#[allow(unused_assignments)] pub fn execute( deps: DepsMut, env: Env, @@ -131,93 +65,47 @@ pub fn execute( cw_ownable::assert_owner(deps.storage, &info.sender)?; match msg { - ExecuteMsg::Snip721Execute(snip721_exec_msg) => match *snip721_exec_msg { - Snip721ExecuteMsg::MintNft { - token_id, - owner, - public_metadata, - private_metadata, - serial_number, - royalty_info, - transferable, - memo, - padding, - } => execute_mint( - deps, - &env, - &info.sender, - token_id.clone(), - owner.clone(), - public_metadata.clone(), - private_metadata.clone(), - serial_number.clone(), - royalty_info.clone(), - transferable, - memo.clone(), - padding.clone(), - ), - Snip721ExecuteMsg::BurnNft { - token_id, - memo, - padding, - } => execute_burn( - deps, - env, - info, - token_id.clone(), - memo.clone(), - padding.clone(), - ), - Snip721ExecuteMsg::TransferNft { - recipient, - token_id, - memo, - padding, - } => execute_transfer( - deps, - env, - info, - recipient.clone(), - token_id.clone(), - memo.clone(), - padding.clone(), - ), - Snip721ExecuteMsg::SendNft { - contract, - receiver_info, - token_id, - msg, - memo, - padding, - } => execute_send( - deps, - env, - info, - contract.clone(), - receiver_info.clone(), - token_id.clone(), - msg.clone().unwrap(), - memo.clone(), - padding.clone(), - ), - _ => { - let snip721_info = SNIP721_INFO.load(deps.storage)?; - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string(), - code_hash: snip721_info.code_hash, - msg: to_binary(&snip721_exec_msg)?, - funds: vec![], - }; - Ok(Response::default().add_message(exec_msg)) - } - }, - ExecuteMsg::ExtensionExecute(extension_msg) => match extension_msg { + ExecuteMsg::MintNft { + token_id, + owner, + public_metadata, + private_metadata, + serial_number, + royalty_info, + transferable, + memo, + padding, + extension, + } => execute_mint( + deps, + env, + info, + token_id, + owner, + public_metadata, + private_metadata, + serial_number, + royalty_info, + transferable, + memo, + padding, + extension, + ), + ExecuteMsg::BurnNft { + token_id, + memo, + padding, + } => execute_burn(deps, env, info, token_id, memo, padding), + ExecuteMsg::Extension { msg } => match msg { ExecuteExt::AddHook { addr, code_hash } => { execute_add_hook(deps, info, addr, code_hash) } ExecuteExt::RemoveHook { addr, code_hash } => { execute_remove_hook(deps, info, addr, code_hash) } + ExecuteExt::UpdateTokenRole { token_id, role } => { + execute_update_token_role(deps, env, info, token_id, role) + } ExecuteExt::UpdateTokenUri { token_id, token_uri, @@ -225,67 +113,78 @@ pub fn execute( ExecuteExt::UpdateTokenWeight { token_id, weight } => { execute_update_token_weight(deps, env, info, token_id, weight) } - ExecuteExt::UpdateTokenRole { token_id, role } => { - execute_update_token_role(deps, env, info, token_id, role) - } ExecuteExt::UpdateQueryAuth { query_auth } => { - cw_ownable::assert_owner(deps.storage, &info.sender)?; - let mut queryauth = QUERY_AUTH.load(deps.storage)?; - queryauth = query_auth.into_valid(deps.api)?; - QUERY_AUTH.save(deps.storage, &queryauth)?; - Ok(Response::default().add_attribute("action", "update query_auth")) + let mut query_auth_res = Snip721roles::default().query_auth.load(deps.storage)?; + let from_raw_query_auth = query_auth.into_valid(deps.api)?; + query_auth_res.address = from_raw_query_auth.address; + query_auth_res.code_hash = from_raw_query_auth.code_hash; + Snip721roles::default() + .query_auth + .save(deps.storage, &query_auth_res)?; + Ok(Response::default()) } }, + ExecuteMsg::TransferNft { + recipient, + token_id, + memo, + padding, + } => execute_transfer(deps, env, info, recipient, token_id, memo, padding), + ExecuteMsg::SendNft { + contract, + receiver_info, + token_id, + msg, + memo, + padding, + } => execute_send( + deps, + env, + info, + contract, + receiver_info, + token_id, + msg, + padding, + memo, + ), + _ => Snip721roles::default() + .execute(deps, env, info, msg) + .map_err(Into::into), } } #[allow(clippy::too_many_arguments)] pub fn execute_mint( deps: DepsMut, - env: &Env, - _sender: &Addr, + env: Env, + info: MessageInfo, token_id: Option, owner: Option, - public_metadata: Option, - private_metadata: Option, - serial_number: Option, - royalty_info: Option, + public_metadata: Option, + private_metadata: Option, + serial_number: Option, + royalty_info: Option, transferable: Option, memo: Option, padding: Option, + extension: MetadataExt, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; let mut total = Uint64::from(TotalStore::load(deps.storage)); let mut diff = MemberDiff::new(owner.clone().unwrap(), None, None); + let _ = diff; // reading the value in diff so we don't get warning let old = MembersStore::load( deps.storage, deps.api.addr_validate(&owner.clone().unwrap())?, ); // Increment the total weight by the weight of the new token - total = total.checked_add(Uint64::from( - public_metadata.clone().unwrap().extension.unwrap().weight, - ))?; + total = total.checked_add(Uint64::from(extension.weight))?; // Add the new NFT weight to the old weight for the owner - let new_weight = old + public_metadata.clone().unwrap().extension.unwrap().weight; + let new_weight = old + extension.weight; // Set the diff for use in hooks diff = MemberDiff::new(owner.clone().unwrap(), Some(old), Some(new_weight)); - // Update member weights and total - // MEMBERS.update( - // deps.storage, - // &deps.api.addr_validate(&owner)?, - // env.block.height, - // |old| -> StdResult<_> { - // // Increment the total weight by the weight of the new token - // total = total.checked_add(Uint64::from(extension.weight))?; - // // Add the new NFT weight to the old weight for the owner - // let new_weight = old.unwrap_or_default() + extension.unwrap().weight; - // // Set the diff for use in hooks - // diff = MemberDiff::new(owner.clone(), old, Some(new_weight)); - // Ok(new_weight) - // }, - // )?; MembersStore::save( deps.storage, env.block.height, @@ -303,22 +202,12 @@ pub fn execute_mint( .into_cosmos_msg(h.addr, h.code_hash) .map(SubMsg::new) })?; - - // //add this contract to be minter - // let minter_msg = WasmMsg::Execute { - // contract_addr: snip721_info.contract_address.to_string().clone(), - // code_hash: snip721_info.code_hash.clone(), - // msg: to_binary(&Snip721ExecuteMsg::AddMinters { - // minters: vec![env.contract.address.clone().to_string()], - // padding: None, - // })?, - // funds: vec![], - // }; - // Call Snip721 mint - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::MintNft { + // Call base mint + let res = Snip721roles::default().execute( + deps, + env, + info, + ExecuteMsg::MintNft { token_id, owner, public_metadata, @@ -328,14 +217,11 @@ pub fn execute_mint( transferable, memo, padding, - })?, - funds: vec![], - }; + extension, + }, + )?; - Ok(Response::default() - .add_submessages(msgs) - // .add_message(minter_msg) - .add_message(exec_msg)) + Ok(res.add_submessages(msgs)) } pub fn execute_burn( @@ -343,75 +229,57 @@ pub fn execute_burn( env: Env, info: MessageInfo, token_id: String, - _memo: Option, - _padding: Option, + memo: Option, + padding: Option, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; // Lookup the owner of the NFT - let owner_res: Snip721QueryAnswer = deps.querier.query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::OwnerOf { + let owner: OwnerOf = from_binary(&Snip721roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::OwnerOf { token_id: token_id.clone(), - viewer: None, include_expired: None, + viewer: None, }, - )?; - let mut owner_addr = Addr::unchecked(""); - if let Snip721QueryAnswer::OwnerOf { owner, .. } = owner_res { - owner_addr = owner; - } + )?)?; // Get the weight of the token - let nft_info_res: Snip721QueryAnswer = deps.querier.query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::NftInfo { + let nft_info: NftInfo = from_binary(&Snip721roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::NftInfo { token_id: token_id.clone(), }, - )?; - let mut extension_res = None; - if let Snip721QueryAnswer::NftInfo { extension, .. } = nft_info_res { - extension_res = extension; - } + )?)?; let mut total = Uint64::from(TotalStore::load(deps.storage)); - let mut diff = MemberDiff::new(owner_addr.clone(), None, None); + let mut diff = MemberDiff::new(owner.owner.clone(), None, None); let _ = diff; // reading the value in diff so we don't get warning // Update member weights and total - let old_weight = MembersStore::load(deps.storage, owner_addr.clone()); + let old_weight = MembersStore::load(deps.storage, owner.owner.clone()); // Subtract the nft weight from the member's old weight let new_weight = old_weight - .checked_sub(extension_res.clone().unwrap().weight) + .checked_sub(nft_info.metadata_extension.weight) .ok_or(ContractError::CannotBurn {})?; // Subtract nft weight from the total - total = total.checked_sub(Uint64::from(extension_res.clone().unwrap().weight))?; + total = total.checked_sub(Uint64::from(nft_info.metadata_extension.weight))?; // Check if the new weight is now zero if new_weight == 0 { // New weight is now None - diff = MemberDiff::new(owner_addr.clone(), Some(old_weight), None); + diff = MemberDiff::new(owner.owner.clone(), Some(old_weight), None); // Remove owner from list of members - MembersStore::remove(deps.storage, owner_addr.clone())?; + MembersStore::remove(deps.storage, owner.owner.clone())?; } else { - // MEMBERS.update( - // deps.storage, - // &owner_addr, - // env.block.height, - // |old| -> StdResult<_> { - // diff = MemberDiff::new(owner.owner.clone(), old, Some(new_weight)); - // Ok(new_weight) - // }, - // )?; - let old = MembersStore::load(deps.storage, owner_addr.clone()); - diff = MemberDiff::new(owner_addr.clone(), Some(old), Some(new_weight)); + let old = MembersStore::load(deps.storage, owner.owner.clone()); + diff = MemberDiff::new(owner.owner.clone(), Some(old), Some(new_weight)); MembersStore::save( deps.storage, env.block.height, - owner_addr.clone(), + owner.owner.clone(), new_weight, )?; } @@ -429,112 +297,87 @@ pub fn execute_burn( })?; // Burn the token - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::BurnNft { + Snip721roles::default().execute( + deps, + env, + info.clone(), + ExecuteMsg::BurnNft { token_id: token_id.clone(), - memo: None, - padding: None, - })?, - funds: vec![], - }; + memo, + padding, + }, + )?; Ok(Response::new() .add_attribute("action", "burn") .add_attribute("sender", info.sender) .add_attribute("token_id", token_id) - .add_submessages(msgs) - .add_message(exec_msg)) + .add_submessages(msgs)) } pub fn execute_transfer( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, recipient: String, token_id: String, - _memo: Option, - _padding: Option, + memo: Option, + padding: Option, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; - - // let contract = Cw721Roles::default(); - // let mut token = contract.tokens.load(deps.storage, &token_id)?; - // // set owner and remove existing approvals - // token.owner = deps.api.addr_validate(&recipient)?; - // token.approvals = vec![]; - // contract.tokens.save(deps.storage, &token_id, &token)?; - - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::TransferNft { + let contract = Snip721roles::default(); + + contract.execute( + deps, + env, + info.clone(), + ExecuteMsg::TransferNft { recipient: recipient.clone(), token_id: token_id.clone(), - memo: None, - padding: None, - })?, - funds: vec![], - }; + memo, + padding, + }, + )?; Ok(Response::new() .add_attribute("action", "transfer_nft") .add_attribute("sender", info.sender) - .add_attribute("recipient", recipient.clone()) - .add_attribute("token_id", token_id.clone()) - .add_message(exec_msg)) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) } #[allow(clippy::too_many_arguments)] pub fn execute_send( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, recipient_contract: String, - recipient_info: Option, + receiver_info: Option, token_id: String, - msg: Binary, - _memo: Option, - _padding: Option, + msg: Option, + memo: Option, + padding: Option, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; - - // let contract = Cw721Roles::default(); - // let mut token = contract.tokens.load(deps.storage, &token_id)?; - // // set owner and remove existing approvals - // token.owner = deps.api.addr_validate(&recipient_contract)?; - // token.approvals = vec![]; - // contract.tokens.save(deps.storage, &token_id, &token)?; - // let send = Snip721ReceiveMsg { - // sender: info.sender.to_string(), - // token_id: token_id.clone(), - // msg, - // }; - - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::SendNft { + let contract = Snip721roles::default(); + + contract.execute( + deps, + env, + info.clone(), + ExecuteMsg::SendNft { contract: recipient_contract.clone(), - receiver_info: Some(snip721::ReceiverInfo { - recipient_code_hash: recipient_info.unwrap().recipient_code_hash, - also_implements_batch_receive_nft: None, - }), + receiver_info, token_id: token_id.clone(), - msg: Some(msg), - memo: None, - padding: None, - })?, - funds: vec![], - }; + msg, + memo, + padding, + }, + )?; Ok(Response::new() .add_attribute("action", "send_nft") .add_attribute("sender", info.sender) - .add_attribute("recipient", recipient_contract.clone()) - .add_attribute("token_id", token_id.clone()) - .add_message(exec_msg)) + .add_attribute("recipient", recipient_contract) + .add_attribute("token_id", token_id)) } pub fn execute_add_hook( @@ -584,70 +427,25 @@ pub fn execute_update_token_role( token_id: String, role: Option, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; + let contract = Snip721roles::default(); + // Make sure NFT exists - let token_res: Snip721QueryAnswer = deps - .querier - .query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::NftInfo { - token_id: token_id.clone(), - }, - ) - .map_err(|_| ContractError::NftDoesNotExist {})?; - let mut extension_res = None; - let mut token_uri_res = Some(String::new()); - if let Snip721QueryAnswer::NftInfo { - extension, - token_uri, - } = token_res - { - extension_res = extension; - token_uri_res = token_uri; + let token = contract.token_extension_info.get(deps.storage, &token_id); + if token.is_none() { + return Err(ContractError::NftDoesNotExist {}); } // Update role with new value - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::SetMetadata { - token_id: token_id.clone(), - public_metadata: Some(snip721::Metadata { - token_uri: Some(token_uri_res.unwrap()), - extension: Some(snip721::Extension { - image: Some(extension_res.clone().unwrap().image.unwrap()), - image_data: Some(extension_res.clone().unwrap().image_data.unwrap()), - external_url: Some(extension_res.clone().unwrap().external_url.unwrap()), - description: Some(extension_res.clone().unwrap().description.unwrap()), - name: Some(extension_res.clone().unwrap().name.unwrap()), - attributes: Some(extension_res.clone().unwrap().attributes.unwrap()), - background_color: Some( - extension_res.clone().unwrap().background_color.unwrap(), - ), - animation_url: Some(extension_res.clone().unwrap().animation_url.unwrap()), - youtube_url: Some(extension_res.clone().unwrap().youtube_url.unwrap()), - media: Some(extension_res.clone().unwrap().media.unwrap()), - protected_attributes: Some( - extension_res.clone().unwrap().protected_attributes.unwrap(), - ), - token_subtype: Some(extension_res.clone().unwrap().token_subtype.unwrap()), - role: role.clone(), - weight: extension_res.unwrap().weight, - }), - }), - private_metadata: None, - padding: None, - })?, - funds: vec![], - }; + token.clone().unwrap().role = role.clone(); + contract + .token_extension_info + .insert(deps.storage, &token_id, &token.unwrap())?; Ok(Response::default() .add_attribute("action", "update_token_role") .add_attribute("sender", info.sender) .add_attribute("token_id", token_id) - .add_attribute("role", role.clone().unwrap_or_default()) - .add_message(exec_msg)) + .add_attribute("role", role.unwrap_or_default())) } pub fn execute_update_token_uri( @@ -657,65 +455,32 @@ pub fn execute_update_token_uri( token_id: String, token_uri: Option, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; - // Make sure NFT exists - let token_res: Snip721QueryAnswer = deps - .querier - .query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::NftInfo { - token_id: token_id.clone(), - }, - ) - .map_err(|_| ContractError::NftDoesNotExist {})?; - - let mut extension_res = None; - if let Snip721QueryAnswer::NftInfo { extension, .. } = token_res { - extension_res = extension; + let contract = Snip721roles::default(); + + let pub_metdata = contract.pub_metadata.get(deps.storage, &token_id); + if pub_metdata.is_none() { + return Err(ContractError::NftDoesNotExist {}); + } + let priv_metdata = contract.priv_metadata.get(deps.storage, &token_id); + if priv_metdata.is_none() { + return Err(ContractError::NftDoesNotExist {}); } - // Update role with new value - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::SetMetadata { - token_id: token_id.clone(), - public_metadata: Some(snip721::Metadata { - token_uri: token_uri.clone(), - extension: Some(snip721::Extension { - image: Some(extension_res.clone().unwrap().image.unwrap()), - image_data: Some(extension_res.clone().unwrap().image_data.unwrap()), - external_url: Some(extension_res.clone().unwrap().external_url.unwrap()), - description: Some(extension_res.clone().unwrap().description.unwrap()), - name: Some(extension_res.clone().unwrap().name.unwrap()), - attributes: Some(extension_res.clone().unwrap().attributes.unwrap()), - background_color: Some( - extension_res.clone().unwrap().background_color.unwrap(), - ), - animation_url: Some(extension_res.clone().unwrap().animation_url.unwrap()), - youtube_url: Some(extension_res.clone().unwrap().youtube_url.unwrap()), - media: Some(extension_res.clone().unwrap().media.unwrap()), - protected_attributes: Some( - extension_res.clone().unwrap().protected_attributes.unwrap(), - ), - token_subtype: Some(extension_res.clone().unwrap().token_subtype.unwrap()), - role: Some(extension_res.clone().unwrap().role.unwrap()), - weight: extension_res.clone().unwrap().weight, - }), - }), - private_metadata: None, - padding: None, - })?, - funds: vec![], - }; + // Set new token URI + pub_metdata.clone().unwrap().token_uri = token_uri.clone(); + priv_metdata.clone().unwrap().token_uri = token_uri.clone(); + contract + .pub_metadata + .insert(deps.storage, &token_id, &pub_metdata.unwrap())?; + contract + .priv_metadata + .insert(deps.storage, &token_id, &priv_metdata.unwrap())?; Ok(Response::new() .add_attribute("action", "update_token_uri") .add_attribute("sender", info.sender) - .add_attribute("token_id", token_id.clone()) - .add_attribute("token_uri", token_uri.clone().unwrap_or_default()) - .add_message(exec_msg)) + .add_attribute("token_id", token_id) + .add_attribute("token_uri", token_uri.unwrap_or_default())) } pub fn execute_update_token_weight( @@ -725,58 +490,38 @@ pub fn execute_update_token_weight( token_id: String, weight: u64, ) -> Result { - let snip721_info = SNIP721_INFO.load(deps.storage)?; + let contract = Snip721roles::default(); + // Make sure NFT exists - let token_res: Snip721QueryAnswer = deps - .querier - .query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::NftInfo { - token_id: token_id.clone(), - }, - ) - .map_err(|_| ContractError::NftDoesNotExist {})?; - - let mut extension_res = None; - let mut token_uri_res = Some(String::new()); - if let Snip721QueryAnswer::NftInfo { - extension, - token_uri, - } = token_res - { - extension_res = extension; - token_uri_res = token_uri; + let token = contract.token_extension_info.get(deps.storage, &token_id); + if token.is_none() { + return Err(ContractError::NftDoesNotExist {}); } + // Lookup the owner of the NFT - let owner_res: Snip721QueryAnswer = deps.querier.query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &Snip721QueryMsg::OwnerOf { + let owner: OwnerOf = from_binary(&contract.query( + deps.as_ref(), + env.clone(), + snip721_roles_impl::msg::QueryMsg::OwnerOf { token_id: token_id.clone(), viewer: None, include_expired: None, }, - )?; - - let mut owner_addr = Addr::unchecked(""); - if let Snip721QueryAnswer::OwnerOf { owner, .. } = owner_res { - owner_addr = owner; - } + )?)?; let mut total = Uint64::from(TotalStore::load(deps.storage)); - let mut diff = MemberDiff::new(owner_addr.clone(), None, None); + let mut diff = MemberDiff::new(owner.owner.clone(), None, None); // Update member weights and total - let old = MembersStore::load(deps.storage, owner_addr.clone()); + let old = MembersStore::load(deps.storage, owner.owner.clone()); let new_total_weight; let old_total_weight = old; - match weight.cmp(&extension_res.clone().unwrap().weight) { + match weight.cmp(&token.clone().unwrap().weight) { Ordering::Greater => { // Subtract the old token weight from the new token weight let weight_difference = weight - .checked_sub(extension_res.clone().unwrap().weight) + .checked_sub(token.clone().unwrap().weight) .ok_or(ContractError::NegativeValue {})?; // Increment the total weight by the weight difference of the new token @@ -784,11 +529,11 @@ pub fn execute_update_token_weight( // Add the new NFT weight to the old weight for the owner new_total_weight = old_total_weight + weight_difference; // Set the diff for use in hooks - diff = MemberDiff::new(owner_addr.clone(), Some(old), Some(new_total_weight)); + diff = MemberDiff::new(owner.owner.clone(), Some(old), Some(new_total_weight)); } Ordering::Less => { // Subtract the new token weight from the old token weight - let weight_difference = extension_res + let weight_difference = token .clone() .unwrap() .weight @@ -805,60 +550,13 @@ pub fn execute_update_token_weight( } Ordering::Equal => return Err(ContractError::NoWeightChange {}), } - MembersStore::save( deps.storage, env.block.height, - owner_addr.clone(), + owner.owner.clone(), new_total_weight, )?; - // MEMBERS.update( - // deps.storage, - // &token.owner, - // env.block.height, - // |old| -> Result<_, ContractError> { - // let new_total_weight; - // let old_total_weight = old.unwrap_or_default(); - - // // Check if new token weight is great than, less than, or equal to - // // the old token weight - // match weight.cmp(&token.extension.weight) { - // Ordering::Greater => { - // // Subtract the old token weight from the new token weight - // let weight_difference = weight - // .checked_sub(token.extension.weight) - // .ok_or(ContractError::NegativeValue {})?; - - // // Increment the total weight by the weight difference of the new token - // total = total.checked_add(Uint64::from(weight_difference))?; - // // Add the new NFT weight to the old weight for the owner - // new_total_weight = old_total_weight + weight_difference; - // // Set the diff for use in hooks - // diff = MemberDiff::new(token.clone().owner, old, Some(new_total_weight)); - // } - // Ordering::Less => { - // // Subtract the new token weight from the old token weight - // let weight_difference = token - // .extension - // .weight - // .checked_sub(weight) - // .ok_or(ContractError::NegativeValue {})?; - - // // Subtract the weight difference from the old total weight - // new_total_weight = old_total_weight - // .checked_sub(weight_difference) - // .ok_or(ContractError::NegativeValue {})?; - - // // Subtract difference from the total - // total = total.checked_sub(Uint64::from(weight_difference))?; - // } - // Ordering::Equal => return Err(ContractError::NoWeightChange {}), - // } - - // Ok(new_total_weight) - // }, - // )?; TotalStore::save(deps.storage, env.block.height, total.u64())?; let diffs = MemberChangedHookMsg { diffs: vec![diff] }; @@ -872,74 +570,35 @@ pub fn execute_update_token_weight( })?; // Save token weight - let exec_msg = WasmMsg::Execute { - contract_addr: snip721_info.contract_address.to_string().clone(), - code_hash: snip721_info.code_hash.clone(), - msg: to_binary(&Snip721ExecuteMsg::SetMetadata { - token_id: token_id.clone(), - public_metadata: Some(snip721::Metadata { - token_uri: Some(token_uri_res.unwrap()), - extension: Some(snip721::Extension { - image: Some(extension_res.clone().unwrap().image.unwrap()), - image_data: Some(extension_res.clone().unwrap().image_data.unwrap()), - external_url: Some(extension_res.clone().unwrap().external_url.unwrap()), - description: Some(extension_res.clone().unwrap().description.unwrap()), - name: Some(extension_res.clone().unwrap().name.unwrap()), - attributes: Some(extension_res.clone().unwrap().attributes.unwrap()), - background_color: Some( - extension_res.clone().unwrap().background_color.unwrap(), - ), - animation_url: Some(extension_res.clone().unwrap().animation_url.unwrap()), - youtube_url: Some(extension_res.clone().unwrap().youtube_url.unwrap()), - media: Some(extension_res.clone().unwrap().media.unwrap()), - protected_attributes: Some( - extension_res.clone().unwrap().protected_attributes.unwrap(), - ), - token_subtype: Some(extension_res.clone().unwrap().token_subtype.unwrap()), - role: Some(extension_res.unwrap().role.unwrap()), - weight, - }), - }), - private_metadata: None, - padding: None, - })?, - funds: vec![], - }; + token.clone().unwrap().weight = weight; + contract + .token_extension_info + .insert(deps.storage, &token_id, &token.unwrap())?; Ok(Response::default() .add_submessages(msgs) .add_attribute("action", "update_token_weight") .add_attribute("sender", info.sender) .add_attribute("token_id", token_id) - .add_attribute("weight", weight.to_string()) - .add_message(exec_msg)) + .add_attribute("weight", weight.to_string())) } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::ExtensionQuery(extension_query) => match extension_query { + QueryMsg::QueryExtension { msg } => match msg { QueryExt::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), QueryExt::ListMembers { start_after, limit } => { to_binary(&query_list_members(deps, start_after, limit)?) } - QueryExt::TotalWeight { at_height } => to_binary(&query_total_weight(deps, at_height)?), - QueryExt::Member { auth, at_height } => { - let query_auth = QUERY_AUTH.load(deps.storage)?; + QueryExt::Member { at_height, auth } => { + let query_auth = Snip721roles::default().query_auth.load(deps.storage)?; let user = authenticate(deps, auth, query_auth)?; to_binary(&query_member(deps, user, at_height)?) } + QueryExt::TotalWeight { at_height } => to_binary(&query_total_weight(deps, at_height)?), }, - QueryMsg::GetNftContractInfo {} => to_binary(&get_info(deps)?), - _ => { - let snip721_info = SNIP721_INFO.load(deps.storage)?; - let res: Snip721QueryAnswer = deps.querier.query_wasm_smart( - snip721_info.code_hash.clone(), - snip721_info.contract_address.to_string().clone(), - &msg, - )?; - Ok(to_binary(&res)?) - } + _ => Snip721roles::default().query(deps, env, msg), } } @@ -955,14 +614,6 @@ pub fn query_total_weight(deps: Deps, height: Option) -> StdResult StdResult { - let res = SNIP721_INFO.load(deps.storage)?; - Ok(Config { - contract_address: res.contract_address, - code_hash: res.code_hash, - }) -} - pub fn query_member(deps: Deps, addr: Addr, height: Option) -> StdResult { if height.is_some() { let weight = MembersStore::may_load_at_height(deps.storage, addr.clone(), height.unwrap())?; @@ -1035,26 +686,3 @@ pub fn authenticate(deps: Deps, auth: Auth, query_auth: Contract) -> StdResult Result { - match msg.id { - SNIP721_INIT_ID => handle_instantiate_reply(deps, msg), - id => Err(ContractError::UnexpectedReplyId { id }), - } -} - -fn handle_instantiate_reply(deps: DepsMut, msg: Reply) -> Result { - match msg.result { - SubMsgResult::Ok(res) => { - let mut snip721_info = SNIP721_INFO.load(deps.storage).unwrap_or_default(); - let data: snip721::InstantiateResponse = from_binary(&res.data.unwrap())?; - snip721_info.code_hash = data.code_hash; - snip721_info.contract_address = data.contract_address.to_string(); - SNIP721_INFO.save(deps.storage, &snip721_info)?; - Ok(Response::new().add_attribute("action", "instantiate")) - } - - SubMsgResult::Err(e) => Err(ContractError::CustomError { val: e }), - } -} diff --git a/contracts/external/snip721-roles/src/lib.rs b/contracts/external/snip721-roles/src/lib.rs index f91a6d9..61b879c 100644 --- a/contracts/external/snip721-roles/src/lib.rs +++ b/contracts/external/snip721-roles/src/lib.rs @@ -3,7 +3,6 @@ pub mod contract; mod error; pub mod msg; -pub mod snip721; pub mod state; #[cfg(test)] diff --git a/contracts/external/snip721-roles/src/msg.rs b/contracts/external/snip721-roles/src/msg.rs index d1249d6..00c78bc 100644 --- a/contracts/external/snip721-roles/src/msg.rs +++ b/contracts/external/snip721-roles/src/msg.rs @@ -1,52 +1,5 @@ -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Addr; -use dao_snip721_extensions::roles::{ExecuteExt, QueryExt}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use shade_protocol::utils::asset::RawContract; +use dao_snip721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; -use crate::snip721::{self, Snip721ExecuteMsg, Snip721QueryMsg}; - -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] -pub struct InstantiateMsg { - /// Code ID for snip721 token contract. - pub code_id: u64, - /// Code hash for snip721 token contract. - pub code_hash: String, - /// Label to use for instantiated snip721 contract. - pub label: String, - /// NFT collection name - pub name: String, - /// NFT collection symbol - pub symbol: String, - - /// entropy used for prng seed - pub entropy: String, - - /// optional privacy configuration for the contract - pub config: Option, - - pub query_auth: RawContract, -} - -#[cw_serde] -pub struct InstantiateResponse { - pub contract_address: Addr, - pub code_hash: String, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] -pub enum ExecuteMsg { - Snip721Execute(Box), - ExtensionExecute(ExecuteExt), -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, QueryResponses)] -pub enum QueryMsg { - #[returns(())] - Snip721Query(Snip721QueryMsg), - #[returns(())] - ExtensionQuery(QueryExt), - #[returns(crate::state::Config)] - GetNftContractInfo {}, -} +pub type InstantiateMsg = snip721_roles_impl::msg::InstantiateMsg; +pub type ExecuteMsg = snip721_roles_impl::msg::ExecuteMsg; +pub type QueryMsg = snip721_roles_impl::msg::QueryMsg; diff --git a/contracts/external/snip721-roles/src/state.rs b/contracts/external/snip721-roles/src/state.rs index 9948750..59ba888 100644 --- a/contracts/external/snip721-roles/src/state.rs +++ b/contracts/external/snip721-roles/src/state.rs @@ -4,7 +4,6 @@ use secret_cw_controllers::Hooks; use secret_storage_plus::Item; use secret_toolkit::storage::Keymap; use serde::{Deserialize, Serialize}; -use shade_protocol::Contract; #[derive(Serialize, Deserialize, JsonSchema, Debug, Default)] pub struct Config { @@ -15,7 +14,6 @@ pub struct Config { // Hooks to contracts that will receive staking and unstaking messages. pub const HOOKS: Hooks = Hooks::new("hooks"); pub const SNIP721_INFO: Item = Item::new("si"); -pub const QUERY_AUTH: Item = Item::new("qa"); // /// A historic snapshot of total weight over time // pub const TOTAL: SnapshotItem = SnapshotItem::new( diff --git a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml index 667759b..0af9cdf 100644 --- a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml @@ -22,6 +22,11 @@ cosmwasm-schema = { workspace = true } dao-pre-propose-base = { workspace = true } dao-voting = { workspace = true } secret-cw2 = { workspace = true } +snip20-reference-impl ={ workspace = true } +dao-proposal-single ={ workspace = true } +shade-protocol ={ workspace = true } +query_auth = { workspace = true } + [dev-dependencies] secret-multi-test = { workspace = true } @@ -30,5 +35,4 @@ cw4-group = { workspace = true } cw-denom = { workspace = true } dao-interface = { workspace = true } # dao-testing = { workspace = true } -dao-proposal-single = { workspace = true } cw-hooks = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json index 5d5c772..3e1bfd0 100644 --- a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json +++ b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json @@ -8,7 +8,8 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "open_proposal_submission", + "proposal_module_code_hash" ], "properties": { "deposit_info": { @@ -33,9 +34,11 @@ "open_proposal_submission": { "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", "type": "boolean" + }, + "proposal_module_code_hash": { + "type": "string" } }, - "additionalProperties": false, "definitions": { "DepositRefundPolicy": { "oneOf": [ @@ -81,8 +84,7 @@ "denom": { "$ref": "#/definitions/UncheckedDenom" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -103,8 +105,7 @@ "token_type": { "$ref": "#/definitions/VotingModuleTokenType" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -116,7 +117,7 @@ "type": "object" }, "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); ```", + "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 secret_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" }, "UncheckedDenom": { @@ -139,11 +140,21 @@ "description": "A cw20 asset.", "type": "object", "required": [ - "cw20" + "snip20" ], "properties": { - "cw20": { - "type": "string" + "snip20": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 } }, "additionalProperties": false @@ -183,8 +194,7 @@ } ] } - }, - "additionalProperties": false + } }, "VotingModuleTokenType": { "type": "string", @@ -209,14 +219,17 @@ "propose": { "type": "object", "required": [ + "auth", "msg" ], "properties": { + "auth": { + "$ref": "#/definitions/Auth" + }, "msg": { "$ref": "#/definitions/ProposeMessage" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -247,8 +260,7 @@ "open_proposal_submission": { "type": "boolean" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -273,9 +285,15 @@ "type": "null" } ] + }, + "key": { + "description": "Snip20 token vaiewing key for sender if denom is SNip20 instead of native", + "type": [ + "string", + "null" + ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -296,8 +314,7 @@ "msg": { "$ref": "#/definitions/Empty" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -312,14 +329,17 @@ "add_proposal_submitted_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -334,14 +354,17 @@ "remove_proposal_submitted_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -368,14 +391,54 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false } ], "definitions": { + "Auth": { + "oneOf": [ + { + "type": "object", + "required": [ + "viewing_key" + ], + "properties": { + "viewing_key": { + "type": "object", + "required": [ + "address", + "key" + ], + "properties": { + "address": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permit" + ], + "properties": { + "permit": { + "$ref": "#/definitions/Permit_for_PermitData" + } + }, + "additionalProperties": false + } + ] + }, "BankMsg": { "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", "oneOf": [ @@ -517,6 +580,7 @@ ], "properties": { "type_url": { + "description": "this is the fully qualified msg path used for routing, e.g. /cosmos.bank.v1beta1.MsgSend NOTE: the type_url can be changed after a chain upgrade", "type": "string" }, "value": { @@ -562,6 +626,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finalize_tx" + ], + "properties": { + "finalize_tx": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false } ] }, @@ -609,8 +685,7 @@ "denom": { "$ref": "#/definitions/UncheckedDenom" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -631,8 +706,7 @@ "token_type": { "$ref": "#/definitions/VotingModuleTokenType" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -731,8 +805,7 @@ ], "properties": { "never": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -740,7 +813,6 @@ ] }, "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ { "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", @@ -762,12 +834,7 @@ "minimum": 0.0 }, "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] + "$ref": "#/definitions/VoteOption" } } } @@ -791,6 +858,7 @@ "required": [ "amount", "channel_id", + "memo", "timeout", "to_address" ], @@ -804,7 +872,11 @@ ] }, "channel_id": { - "description": "existing channel to send the tokens over", + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "memo": { + "description": "optional memo can put here `{\"ibc_callback\":\"secret1contractAddr\"}` to get a callback on ack/timeout see this for more info: https://github.com/scrtlabs/SecretNetwork/blob/78a5f82a4/x/ibc-hooks/README.md?plain=1#L144-L188", "type": "string" }, "timeout": { @@ -922,13 +994,94 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 } } }, + "PermitData": { + "type": "object", + "required": [ + "data", + "key" + ], + "properties": { + "data": { + "$ref": "#/definitions/Binary" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PermitSignature": { + "type": "object", + "required": [ + "pub_key", + "signature" + ], + "properties": { + "pub_key": { + "$ref": "#/definitions/PubKey" + }, + "signature": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "Permit_for_PermitData": { + "description": "Where the information will be stored", + "type": "object", + "required": [ + "params", + "signature" + ], + "properties": { + "account_number": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "chain_id": { + "type": [ + "string", + "null" + ] + }, + "memo": { + "type": [ + "string", + "null" + ] + }, + "params": { + "$ref": "#/definitions/PermitData" + }, + "sequence": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "signature": { + "$ref": "#/definitions/PermitSignature" + } + }, + "additionalProperties": false + }, "ProposeMessage": { "oneOf": [ { @@ -966,6 +1119,28 @@ } ] }, + "PubKey": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "description": "ignored, but must be \"tendermint/PubKeySecp256k1\" otherwise the verification will fail", + "type": "string" + }, + "value": { + "description": "Secp256k1 PubKey", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", "oneOf": [ @@ -1110,8 +1285,7 @@ "expiration": { "$ref": "#/definitions/Expiration" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1126,7 +1300,7 @@ ] }, "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); ```", + "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 secret_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" @@ -1134,11 +1308,11 @@ ] }, "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); ```", + "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 secret_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); ```", + "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 secret_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" }, "UncheckedDenom": { @@ -1161,11 +1335,21 @@ "description": "A cw20 asset.", "type": "object", "required": [ - "cw20" + "snip20" ], "properties": { - "cw20": { - "type": "string" + "snip20": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 } }, "additionalProperties": false @@ -1205,8 +1389,7 @@ } ] } - }, - "additionalProperties": false + } }, "VoteOption": { "type": "string", @@ -1237,19 +1420,18 @@ "execute": { "type": "object", "required": [ + "code_hash", "contract_addr", - "funds", - "msg" + "msg", + "send" ], "properties": { - "contract_addr": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", "type": "string" }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "contract_addr": { + "type": "string" }, "msg": { "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", @@ -1258,6 +1440,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -1265,7 +1453,7 @@ "additionalProperties": false }, { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", "type": "object", "required": [ "instantiate" @@ -1274,10 +1462,11 @@ "instantiate": { "type": "object", "required": [ + "code_hash", "code_id", - "funds", "label", - "msg" + "msg", + "send" ], "properties": { "admin": { @@ -1286,19 +1475,17 @@ "null" ] }, + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, "code_id": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, "label": { - "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "description": "A human-readbale label for the contract, must be unique across all contracts", "type": "string" }, "msg": { @@ -1308,6 +1495,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -1324,11 +1517,22 @@ "migrate": { "type": "object", "required": [ + "code_hash", + "code_id", "contract_addr", - "msg", - "new_code_id" + "msg" ], "properties": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the **new** code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, + "code_id": { + "description": "the code_id of the **new** logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "contract_addr": { "type": "string" }, @@ -1339,12 +1543,6 @@ "$ref": "#/definitions/Binary" } ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -1413,8 +1611,7 @@ ], "properties": { "proposal_module": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1427,8 +1624,7 @@ ], "properties": { "dao": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1441,8 +1637,7 @@ ], "properties": { "config": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1465,8 +1660,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1479,8 +1673,7 @@ ], "properties": { "proposal_submitted_hooks": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1501,8 +1694,7 @@ "msg": { "$ref": "#/definitions/Empty" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1542,7 +1734,6 @@ "type": "boolean" } }, - "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.", @@ -1568,11 +1759,21 @@ "description": "A cw20 asset.", "type": "object", "required": [ - "cw20" + "snip20" ], "properties": { - "cw20": { - "$ref": "#/definitions/Addr" + "snip20": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 } }, "additionalProperties": false @@ -1612,8 +1813,7 @@ } ] } - }, - "additionalProperties": false + } }, "DepositRefundPolicy": { "oneOf": [ @@ -1641,16 +1841,33 @@ ] }, "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); ```", + "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 secret_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" } } }, "dao": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "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" + "title": "AnyContractInfo", + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "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" + } + } }, "deposit_info": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -1680,7 +1897,6 @@ ] } }, - "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.", @@ -1706,11 +1922,21 @@ "description": "A cw20 asset.", "type": "object", "required": [ - "cw20" + "snip20" ], "properties": { - "cw20": { - "$ref": "#/definitions/Addr" + "snip20": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 } }, "additionalProperties": false @@ -1750,8 +1976,7 @@ } ] } - }, - "additionalProperties": false + } }, "DepositRefundPolicy": { "oneOf": [ @@ -1779,16 +2004,33 @@ ] }, "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); ```", + "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 secret_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" } } }, "proposal_module": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "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" + "title": "AnyContractInfo", + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "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" + } + } }, "proposal_submitted_hooks": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -1801,11 +2043,32 @@ "hooks": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/HookItem" } } }, - "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" + }, + "HookItem": { + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + } + } }, "query_extension": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/pre-propose/dao-pre-propose-single/src/lib.rs b/contracts/pre-propose/dao-pre-propose-single/src/lib.rs index ff777d6..16d6ba5 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/lib.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/lib.rs @@ -2,8 +2,8 @@ pub mod contract; -// #[cfg(test)] -// mod tests; +#[cfg(test)] +mod tests; pub use contract::{ExecuteMsg, InstantiateMsg, ProposeMessage, QueryMsg}; diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index d766ce5..8ee1f13 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -1,14 +1,11 @@ -use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; -use cw2::ContractVersion; -use cw20::Cw20Coin; +use cosmwasm_std::{ + coins, from_binary, to_binary, Addr, Coin, ContractInfo, Empty, MessageInfo, Uint128, +}; use cw_denom::UncheckedDenom; -use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; -use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; use dao_proposal_single as dps; -use dao_testing::helpers::instantiate_with_cw4_groups_governance; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, @@ -16,7 +13,11 @@ use dao_voting::{ threshold::{PercentageThreshold, Threshold}, voting::Vote, }; -use dps::query::ProposalResponse; +use secret_cw2::ContractVersion; +use secret_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; +use secret_utils::Duration; +use shade_protocol::utils::asset::RawContract; +use snip20_reference_impl::msg::InitialBalance; use crate::contract::*; @@ -38,35 +39,89 @@ fn cw_pre_propose_base_proposal_single() -> Box> { fn cw20_base_contract() -> Box> { let contract = ContractWrapper::new( - cw20_base::contract::execute, - cw20_base::contract::instantiate, - cw20_base::contract::query, + snip20_reference_impl::contract::execute, + snip20_reference_impl::contract::instantiate, + snip20_reference_impl::contract::query, + ); + Box::new(contract) +} + +pub fn query_auth_contract() -> Box> { + let contract = ContractWrapper::new( + query_auth::contract::execute, + query_auth::contract::instantiate, + query_auth::contract::query, ); Box::new(contract) } +pub fn instantiate_query_auth(app: &mut App) -> ContractInfo { + let query_auth_info = app.store_code(query_auth_contract()); + let msg = shade_protocol::contract_interfaces::query_auth::InstantiateMsg { + admin_auth: shade_protocol::Contract { + address: Addr::unchecked("admin_contract"), + code_hash: "code_hash".to_string(), + }, + prng_seed: to_binary("seed").unwrap(), + }; + + app.instantiate_contract( + query_auth_info, + Addr::unchecked("CREATOR_ADDR"), + &msg, + &[], + "query_auth", + None, + ) + .unwrap() +} + +pub fn create_viewing_key(app: &mut App, contract_info: ContractInfo, info: MessageInfo) -> String { + let msg = shade_protocol::contract_interfaces::query_auth::ExecuteMsg::CreateViewingKey { + entropy: "entropy".to_string(), + padding: None, + }; + let res = app + .execute_contract(info.sender, &contract_info, &msg, &[]) + .unwrap(); + let mut viewing_key = String::new(); + let data: shade_protocol::contract_interfaces::query_auth::ExecuteAnswer = + from_binary(&res.data.unwrap()).unwrap(); + if let shade_protocol::contract_interfaces::query_auth::ExecuteAnswer::CreateViewingKey { + key, + } = data + { + viewing_key = key; + }; + viewing_key +} + fn get_default_proposal_module_instantiate( app: &mut App, deposit_info: Option, open_proposal_submission: bool, + proposal_module_code_hash: String, ) -> dps::msg::InstantiateMsg { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_instantiate_info = app.store_code(cw_pre_propose_base_proposal_single()); + let query_auth = instantiate_query_auth(app); dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, - max_voting_period: cw_utils::Duration::Time(86400), + max_voting_period: secret_utils::Duration::Time(86400), min_voting_period: None, only_members_execute: false, allow_revoting: false, pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { - code_id: pre_propose_id, - msg: to_json_binary(&InstantiateMsg { + code_id: pre_propose_instantiate_info.code_id, + code_hash: pre_propose_instantiate_info.code_hash, + msg: to_binary(&InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), + proposal_module_code_hash, }) .unwrap(), admin: Some(Admin::CoreModule {}), @@ -76,21 +131,28 @@ fn get_default_proposal_module_instantiate( }, close_proposal_on_execution_failure: false, veto: None, + dao_code_hash: "dao_code_hash".to_string(), + query_auth: RawContract { + address: query_auth.address.to_string(), + code_hash: query_auth.code_hash, + }, } } -fn instantiate_cw20_base_default(app: &mut App) -> Addr { +fn instantiate_cw20_base_default(app: &mut App) -> ContractInfo { let cw20_id = app.store_code(cw20_base_contract()); - let cw20_instantiate = cw20_base::msg::InstantiateMsg { - name: "cw20 token".to_string(), - symbol: "cwtwenty".to_string(), + let cw20_instantiate = snip20_reference_impl::msg::InstantiateMsg { + name: "snip20 token".to_string(), + symbol: "sniptwenty".to_string(), decimals: 6, - initial_balances: vec![Cw20Coin { + initial_balances: Some(vec![InitialBalance { address: "ekez".to_string(), amount: Uint128::new(10), - }], - mint: None, - marketing: None, + }]), + admin: Some("ekez".to_string()), + prng_seed: to_binary(&"prng_seed".to_string()).unwrap(), + config: None, + supported_denoms: None, }; app.instantiate_contract( cw20_id, @@ -104,565 +166,565 @@ fn instantiate_cw20_base_default(app: &mut App) -> Addr { } struct DefaultTestSetup { - core_addr: Addr, - proposal_single: Addr, - pre_propose: Addr, + proposal_single: ContractInfo, + pre_propose: ContractInfo, } fn setup_default_test( app: &mut App, deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let dps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_info = app.store_code(cw_dao_proposal_single_contract()); - let proposal_module_instantiate = - get_default_proposal_module_instantiate(app, deposit_info, open_proposal_submission); - - let core_addr = instantiate_with_cw4_groups_governance( + let proposal_module_instantiate = get_default_proposal_module_instantiate( app, - dps_id, - to_json_binary(&proposal_module_instantiate).unwrap(), - Some(vec![ - cw20::Cw20Coin { - address: "ekez".to_string(), - amount: Uint128::new(9), - }, - cw20::Cw20Coin { - address: "keze".to_string(), - amount: Uint128::new(8), - }, - ]), + deposit_info, + open_proposal_submission, + dps_info.clone().code_hash, ); - let proposal_modules: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &dao_interface::msg::QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, + + let proposal_module = app + .instantiate_contract( + dps_info.clone(), + Addr::unchecked("dao"), + &proposal_module_instantiate, + &[], + "proposal_Single".to_string(), + Some("dao".to_string()), ) .unwrap(); - assert_eq!(proposal_modules.len(), 1); - let proposal_single = proposal_modules.into_iter().next().unwrap().address; let proposal_creation_policy = app .wrap() .query_wasm_smart( - proposal_single.clone(), + proposal_module.clone().code_hash, + proposal_module.clone().address.to_string(), &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); let pre_propose = match proposal_creation_policy { - ProposalCreationPolicy::Module { addr } => addr, + ProposalCreationPolicy::Module { addr, code_hash } => ContractInfo { + address: addr, + code_hash, + }, _ => panic!("expected a module for the proposal creation policy"), }; // Make sure things were set up correctly. assert_eq!( - proposal_single, - get_proposal_module(app, pre_propose.clone()) + proposal_module, + get_proposal_module( + app, + pre_propose.clone().address, + pre_propose.clone().code_hash + ) ); - assert_eq!(core_addr, get_dao(app, pre_propose.clone())); DefaultTestSetup { - core_addr, - proposal_single, + proposal_single: proposal_module, pre_propose, } } -fn make_proposal( - app: &mut App, +// fn make_proposal( +// app: &mut App, +// pre_propose: Addr, +// proposal_module: Addr, +// proposer: &str, +// funds: &[Coin], +// ) -> u64 { +// app.execute_contract( +// Addr::unchecked(proposer), +// pre_propose, +// &ExecuteMsg::Propose { +// msg: ProposeMessage::Propose { +// title: "title".to_string(), +// description: "description".to_string(), +// msgs: vec![], +// }, +// }, +// funds, +// ) +// .unwrap(); + +// let id: u64 = app +// .wrap() +// .query_wasm_smart(&proposal_module, &dps::msg::QueryMsg::NextProposalId {}) +// .unwrap(); +// let id = id - 1; + +// let proposal: ProposalResponse = app +// .wrap() +// .query_wasm_smart( +// proposal_module, +// &dps::msg::QueryMsg::Proposal { proposal_id: id }, +// ) +// .unwrap(); + +// assert_eq!(proposal.proposal.proposer, Addr::unchecked(proposer)); +// assert_eq!(proposal.proposal.title, "title".to_string()); +// assert_eq!(proposal.proposal.description, "description".to_string()); +// assert_eq!(proposal.proposal.msgs, vec![]); + +// id +// } + +// fn mint_natives(app: &mut App, receiver: &str, coins: Vec) { +// // Mint some ekez tokens for ekez so we can pay the deposit. +// app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { +// to_address: receiver.to_string(), +// amount: coins, +// })) +// .unwrap(); +// } + +// fn increase_allowance(app: &mut App, sender: &str, receiver: &Addr, cw20: Addr, amount: Uint128) { +// app.execute_contract( +// Addr::unchecked(sender), +// cw20, +// &cw20::Cw20ExecuteMsg::IncreaseAllowance { +// spender: receiver.to_string(), +// amount, +// expires: None, +// }, +// &[], +// ) +// .unwrap(); +// } + +// fn add_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) { +// app.execute_contract( +// Addr::unchecked(sender), +// module.clone(), +// &ExecuteMsg::AddProposalSubmittedHook { +// address: hook.to_string(), +// }, +// &[], +// ) +// .unwrap(); +// } + +// fn remove_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) { +// app.execute_contract( +// Addr::unchecked(sender), +// module.clone(), +// &ExecuteMsg::RemoveProposalSubmittedHook { +// address: hook.to_string(), +// }, +// &[], +// ) +// .unwrap(); +// } + +// fn get_balance_cw20, U: Into>( +// app: &App, +// contract_addr: T, +// address: U, +// ) -> Uint128 { +// let msg = cw20::Cw20QueryMsg::Balance { +// address: address.into(), +// }; +// let result: cw20::BalanceResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap(); +// result.balance +// } + +// fn get_balance_native(app: &App, who: &str, denom: &str) -> Uint128 { +// let res = app.wrap().query_balance(who, denom).unwrap(); +// res.amount +// } + +// fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> Status { +// app.execute_contract( +// Addr::unchecked(sender), +// module.clone(), +// &dps::msg::ExecuteMsg::Vote { +// rationale: None, +// proposal_id: id, +// vote: position, +// }, +// &[], +// ) +// .unwrap(); + +// let proposal: ProposalResponse = app +// .wrap() +// .query_wasm_smart(module, &dps::msg::QueryMsg::Proposal { proposal_id: id }) +// .unwrap(); + +// proposal.proposal.status +// } + +// fn get_config(app: &App, module: Addr) -> Config { +// app.wrap() +// .query_wasm_smart(module, &QueryMsg::Config {}) +// .unwrap() +// } + +// fn get_dao(app: &App, module: Addr) -> Addr { +// app.wrap() +// .query_wasm_smart(module, &QueryMsg::Dao {}) +// .unwrap() +// } + +// fn query_hooks(app: &App, module: Addr) -> cw_hooks::HooksResponse { +// app.wrap() +// .query_wasm_smart(module, &QueryMsg::ProposalSubmittedHooks {}) +// .unwrap() +// } + +fn get_proposal_module( + app: &App, pre_propose: Addr, - proposal_module: Addr, - proposer: &str, - funds: &[Coin], -) -> u64 { - app.execute_contract( - Addr::unchecked(proposer), - pre_propose, - &ExecuteMsg::Propose { - msg: ProposeMessage::Propose { - title: "title".to_string(), - description: "description".to_string(), - msgs: vec![], - }, - }, - funds, - ) - .unwrap(); - - let id: u64 = app - .wrap() - .query_wasm_smart(&proposal_module, &dps::msg::QueryMsg::NextProposalId {}) - .unwrap(); - let id = id - 1; - - let proposal: ProposalResponse = app - .wrap() + pre_propose_code_hash: String, +) -> ContractInfo { + app.wrap() .query_wasm_smart( - proposal_module, - &dps::msg::QueryMsg::Proposal { proposal_id: id }, + pre_propose, + pre_propose_code_hash, + &QueryMsg::ProposalModule {}, ) - .unwrap(); - - assert_eq!(proposal.proposal.proposer, Addr::unchecked(proposer)); - assert_eq!(proposal.proposal.title, "title".to_string()); - assert_eq!(proposal.proposal.description, "description".to_string()); - assert_eq!(proposal.proposal.msgs, vec![]); - - id -} - -fn mint_natives(app: &mut App, receiver: &str, coins: Vec) { - // Mint some ekez tokens for ekez so we can pay the deposit. - app.sudo(cw_multi_test::SudoMsg::Bank(BankSudo::Mint { - to_address: receiver.to_string(), - amount: coins, - })) - .unwrap(); -} - -fn increase_allowance(app: &mut App, sender: &str, receiver: &Addr, cw20: Addr, amount: Uint128) { - app.execute_contract( - Addr::unchecked(sender), - cw20, - &cw20::Cw20ExecuteMsg::IncreaseAllowance { - spender: receiver.to_string(), - amount, - expires: None, - }, - &[], - ) - .unwrap(); -} - -fn add_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) { - app.execute_contract( - Addr::unchecked(sender), - module.clone(), - &ExecuteMsg::AddProposalSubmittedHook { - address: hook.to_string(), - }, - &[], - ) - .unwrap(); -} - -fn remove_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) { - app.execute_contract( - Addr::unchecked(sender), - module.clone(), - &ExecuteMsg::RemoveProposalSubmittedHook { - address: hook.to_string(), - }, - &[], - ) - .unwrap(); -} - -fn get_balance_cw20, U: Into>( - app: &App, - contract_addr: T, - address: U, -) -> Uint128 { - let msg = cw20::Cw20QueryMsg::Balance { - address: address.into(), - }; - let result: cw20::BalanceResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap(); - result.balance -} - -fn get_balance_native(app: &App, who: &str, denom: &str) -> Uint128 { - let res = app.wrap().query_balance(who, denom).unwrap(); - res.amount -} - -fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> Status { - app.execute_contract( - Addr::unchecked(sender), - module.clone(), - &dps::msg::ExecuteMsg::Vote { - rationale: None, - proposal_id: id, - vote: position, - }, - &[], - ) - .unwrap(); - - let proposal: ProposalResponse = app - .wrap() - .query_wasm_smart(module, &dps::msg::QueryMsg::Proposal { proposal_id: id }) - .unwrap(); - - proposal.proposal.status -} - -fn get_config(app: &App, module: Addr) -> Config { - app.wrap() - .query_wasm_smart(module, &QueryMsg::Config {}) - .unwrap() -} - -fn get_dao(app: &App, module: Addr) -> Addr { - app.wrap() - .query_wasm_smart(module, &QueryMsg::Dao {}) - .unwrap() -} - -fn query_hooks(app: &App, module: Addr) -> cw_hooks::HooksResponse { - app.wrap() - .query_wasm_smart(module, &QueryMsg::ProposalSubmittedHooks {}) - .unwrap() -} - -fn get_proposal_module(app: &App, module: Addr) -> Addr { - app.wrap() - .query_wasm_smart(module, &QueryMsg::ProposalModule {}) - .unwrap() -} - -fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { - app.wrap() - .query_wasm_smart(module, &QueryMsg::DepositInfo { proposal_id: id }) .unwrap() } -fn update_config( - app: &mut App, - module: Addr, - sender: &str, - deposit_info: Option, - open_proposal_submission: bool, -) -> Config { - app.execute_contract( - Addr::unchecked(sender), - module.clone(), - &ExecuteMsg::UpdateConfig { - deposit_info, - open_proposal_submission, - }, - &[], - ) - .unwrap(); - - get_config(app, module) -} - -fn update_config_should_fail( - app: &mut App, - module: Addr, - sender: &str, - deposit_info: Option, - open_proposal_submission: bool, -) -> PreProposeError { - app.execute_contract( - Addr::unchecked(sender), - module, - &ExecuteMsg::UpdateConfig { - deposit_info, - open_proposal_submission, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap() -} - -fn withdraw(app: &mut App, module: Addr, sender: &str, denom: Option) { - app.execute_contract( - Addr::unchecked(sender), - module, - &ExecuteMsg::Withdraw { denom }, - &[], - ) - .unwrap(); -} - -fn withdraw_should_fail( - app: &mut App, - module: Addr, - sender: &str, - denom: Option, -) -> PreProposeError { - app.execute_contract( - Addr::unchecked(sender), - module, - &ExecuteMsg::Withdraw { denom }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap() -} - -fn close_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { - app.execute_contract( - Addr::unchecked(sender), - module, - &dps::msg::ExecuteMsg::Close { proposal_id }, - &[], - ) - .unwrap(); -} - -fn execute_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { - app.execute_contract( - Addr::unchecked(sender), - module, - &dps::msg::ExecuteMsg::Execute { proposal_id }, - &[], - ) - .unwrap(); -} - -enum EndStatus { - Passed, - Failed, -} -enum RefundReceiver { - Proposer, - Dao, -} - -fn test_native_permutation( - end_status: EndStatus, - refund_policy: DepositRefundPolicy, - receiver: RefundReceiver, -) { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test( - &mut app, - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy, - }), - false, - ); - - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let id = make_proposal( - &mut app, - pre_propose, - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), - ); - - // Make sure it went away. - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(balance, Uint128::zero()); - - #[allow(clippy::type_complexity)] - let (position, expected_status, trigger_refund): ( - _, - _, - fn(&mut App, Addr, &str, u64) -> (), - ) = match end_status { - EndStatus::Passed => (Vote::Yes, Status::Passed, execute_proposal), - EndStatus::Failed => (Vote::No, Status::Rejected, close_proposal), - }; - let new_status = vote(&mut app, proposal_single.clone(), "ekez", id, position); - assert_eq!(new_status, expected_status); - - // Close or execute the proposal to trigger a refund. - trigger_refund(&mut app, proposal_single, "ekez", id); - - let (dao_expected, proposer_expected) = match receiver { - RefundReceiver::Proposer => (0, 10), - RefundReceiver::Dao => (10, 0), - }; - - let proposer_balance = get_balance_native(&app, "ekez", "ujuno"); - let dao_balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(proposer_expected, proposer_balance.u128()); - assert_eq!(dao_expected, dao_balance.u128()) -} - -fn test_cw20_permutation( - end_status: EndStatus, - refund_policy: DepositRefundPolicy, - receiver: RefundReceiver, -) { - let mut app = App::default(); - - let cw20_address = instantiate_cw20_base_default(&mut app); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test( - &mut app, - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Cw20(cw20_address.to_string()), - }, - amount: Uint128::new(10), - refund_policy, - }), - false, - ); - - increase_allowance( - &mut app, - "ekez", - &pre_propose, - cw20_address.clone(), - Uint128::new(10), - ); - let id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &[], - ); - - // Make sure it went await. - let balance = get_balance_cw20(&app, cw20_address.clone(), "ekez"); - assert_eq!(balance, Uint128::zero()); - - #[allow(clippy::type_complexity)] - let (position, expected_status, trigger_refund): ( - _, - _, - fn(&mut App, Addr, &str, u64) -> (), - ) = match end_status { - EndStatus::Passed => (Vote::Yes, Status::Passed, execute_proposal), - EndStatus::Failed => (Vote::No, Status::Rejected, close_proposal), - }; - let new_status = vote(&mut app, proposal_single.clone(), "ekez", id, position); - assert_eq!(new_status, expected_status); - - // Close or execute the proposal to trigger a refund. - trigger_refund(&mut app, proposal_single, "ekez", id); - - let (dao_expected, proposer_expected) = match receiver { - RefundReceiver::Proposer => (0, 10), - RefundReceiver::Dao => (10, 0), - }; - - let proposer_balance = get_balance_cw20(&app, &cw20_address, "ekez"); - let dao_balance = get_balance_cw20(&app, &cw20_address, core_addr); - assert_eq!(proposer_expected, proposer_balance.u128()); - assert_eq!(dao_expected, dao_balance.u128()) -} - -#[test] -fn test_native_failed_always_refund() { - test_native_permutation( - EndStatus::Failed, - DepositRefundPolicy::Always, - RefundReceiver::Proposer, - ) -} -#[test] -fn test_cw20_failed_always_refund() { - test_cw20_permutation( - EndStatus::Failed, - DepositRefundPolicy::Always, - RefundReceiver::Proposer, - ) -} - -#[test] -fn test_native_passed_always_refund() { - test_native_permutation( - EndStatus::Passed, - DepositRefundPolicy::Always, - RefundReceiver::Proposer, - ) -} - -#[test] -fn test_cw20_passed_always_refund() { - test_cw20_permutation( - EndStatus::Passed, - DepositRefundPolicy::Always, - RefundReceiver::Proposer, - ) -} - -#[test] -fn test_native_passed_never_refund() { - test_native_permutation( - EndStatus::Passed, - DepositRefundPolicy::Never, - RefundReceiver::Dao, - ) -} -#[test] -fn test_cw20_passed_never_refund() { - test_cw20_permutation( - EndStatus::Passed, - DepositRefundPolicy::Never, - RefundReceiver::Dao, - ) -} - -#[test] -fn test_native_failed_never_refund() { - test_native_permutation( - EndStatus::Failed, - DepositRefundPolicy::Never, - RefundReceiver::Dao, - ) -} -#[test] -fn test_cw20_failed_never_refund() { - test_cw20_permutation( - EndStatus::Failed, - DepositRefundPolicy::Never, - RefundReceiver::Dao, - ) -} - -#[test] -fn test_native_passed_passed_refund() { - test_native_permutation( - EndStatus::Passed, - DepositRefundPolicy::OnlyPassed, - RefundReceiver::Proposer, - ) -} -#[test] -fn test_cw20_passed_passed_refund() { - test_cw20_permutation( - EndStatus::Passed, - DepositRefundPolicy::OnlyPassed, - RefundReceiver::Proposer, - ) -} - -#[test] -fn test_native_failed_passed_refund() { - test_native_permutation( - EndStatus::Failed, - DepositRefundPolicy::OnlyPassed, - RefundReceiver::Dao, - ) -} -#[test] -fn test_cw20_failed_passed_refund() { - test_cw20_permutation( - EndStatus::Failed, - DepositRefundPolicy::OnlyPassed, - RefundReceiver::Dao, - ) -} +// fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { +// app.wrap() +// .query_wasm_smart(module, &QueryMsg::DepositInfo { proposal_id: id }) +// .unwrap() +// } + +// fn update_config( +// app: &mut App, +// module: Addr, +// sender: &str, +// deposit_info: Option, +// open_proposal_submission: bool, +// ) -> Config { +// app.execute_contract( +// Addr::unchecked(sender), +// module.clone(), +// &ExecuteMsg::UpdateConfig { +// deposit_info, +// open_proposal_submission, +// }, +// &[], +// ) +// .unwrap(); + +// get_config(app, module) +// } + +// fn update_config_should_fail( +// app: &mut App, +// module: Addr, +// sender: &str, +// deposit_info: Option, +// open_proposal_submission: bool, +// ) -> PreProposeError { +// app.execute_contract( +// Addr::unchecked(sender), +// module, +// &ExecuteMsg::UpdateConfig { +// deposit_info, +// open_proposal_submission, +// }, +// &[], +// ) +// .unwrap_err() +// .downcast() +// .unwrap() +// } + +// fn withdraw(app: &mut App, module: Addr, sender: &str, denom: Option) { +// app.execute_contract( +// Addr::unchecked(sender), +// module, +// &ExecuteMsg::Withdraw { denom }, +// &[], +// ) +// .unwrap(); +// } + +// fn withdraw_should_fail( +// app: &mut App, +// module: Addr, +// sender: &str, +// denom: Option, +// ) -> PreProposeError { +// app.execute_contract( +// Addr::unchecked(sender), +// module, +// &ExecuteMsg::Withdraw { denom }, +// &[], +// ) +// .unwrap_err() +// .downcast() +// .unwrap() +// } + +// fn close_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { +// app.execute_contract( +// Addr::unchecked(sender), +// module, +// &dps::msg::ExecuteMsg::Close { proposal_id }, +// &[], +// ) +// .unwrap(); +// } + +// fn execute_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { +// app.execute_contract( +// Addr::unchecked(sender), +// module, +// &dps::msg::ExecuteMsg::Execute { proposal_id }, +// &[], +// ) +// .unwrap(); +// } + +// enum EndStatus { +// Passed, +// Failed, +// } +// enum RefundReceiver { +// Proposer, +// Dao, +// } + +// fn test_native_permutation( +// end_status: EndStatus, +// refund_policy: DepositRefundPolicy, +// receiver: RefundReceiver, +// ) { +// let mut app = App::default(); + +// let DefaultTestSetup { +// core_addr, +// proposal_single, +// pre_propose, +// } = setup_default_test( +// &mut app, +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy, +// }), +// false, +// ); + +// mint_natives(&mut app, "ekez", coins(10, "ujuno")); +// let id = make_proposal( +// &mut app, +// pre_propose, +// proposal_single.clone(), +// "ekez", +// &coins(10, "ujuno"), +// ); + +// // Make sure it went away. +// let balance = get_balance_native(&app, "ekez", "ujuno"); +// assert_eq!(balance, Uint128::zero()); + +// #[allow(clippy::type_complexity)] +// let (position, expected_status, trigger_refund): ( +// _, +// _, +// fn(&mut App, Addr, &str, u64) -> (), +// ) = match end_status { +// EndStatus::Passed => (Vote::Yes, Status::Passed, execute_proposal), +// EndStatus::Failed => (Vote::No, Status::Rejected, close_proposal), +// }; +// let new_status = vote(&mut app, proposal_single.clone(), "ekez", id, position); +// assert_eq!(new_status, expected_status); + +// // Close or execute the proposal to trigger a refund. +// trigger_refund(&mut app, proposal_single, "ekez", id); + +// let (dao_expected, proposer_expected) = match receiver { +// RefundReceiver::Proposer => (0, 10), +// RefundReceiver::Dao => (10, 0), +// }; + +// let proposer_balance = get_balance_native(&app, "ekez", "ujuno"); +// let dao_balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); +// assert_eq!(proposer_expected, proposer_balance.u128()); +// assert_eq!(dao_expected, dao_balance.u128()) +// } + +// fn test_cw20_permutation( +// end_status: EndStatus, +// refund_policy: DepositRefundPolicy, +// receiver: RefundReceiver, +// ) { +// let mut app = App::default(); + +// let cw20_address = instantiate_cw20_base_default(&mut app); + +// let DefaultTestSetup { +// core_addr, +// proposal_single, +// pre_propose, +// } = setup_default_test( +// &mut app, +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Cw20(cw20_address.to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy, +// }), +// false, +// ); + +// increase_allowance( +// &mut app, +// "ekez", +// &pre_propose, +// cw20_address.clone(), +// Uint128::new(10), +// ); +// let id = make_proposal( +// &mut app, +// pre_propose.clone(), +// proposal_single.clone(), +// "ekez", +// &[], +// ); + +// // Make sure it went await. +// let balance = get_balance_cw20(&app, cw20_address.clone(), "ekez"); +// assert_eq!(balance, Uint128::zero()); + +// #[allow(clippy::type_complexity)] +// let (position, expected_status, trigger_refund): ( +// _, +// _, +// fn(&mut App, Addr, &str, u64) -> (), +// ) = match end_status { +// EndStatus::Passed => (Vote::Yes, Status::Passed, execute_proposal), +// EndStatus::Failed => (Vote::No, Status::Rejected, close_proposal), +// }; +// let new_status = vote(&mut app, proposal_single.clone(), "ekez", id, position); +// assert_eq!(new_status, expected_status); + +// // Close or execute the proposal to trigger a refund. +// trigger_refund(&mut app, proposal_single, "ekez", id); + +// let (dao_expected, proposer_expected) = match receiver { +// RefundReceiver::Proposer => (0, 10), +// RefundReceiver::Dao => (10, 0), +// }; + +// let proposer_balance = get_balance_cw20(&app, &cw20_address, "ekez"); +// let dao_balance = get_balance_cw20(&app, &cw20_address, core_addr); +// assert_eq!(proposer_expected, proposer_balance.u128()); +// assert_eq!(dao_expected, dao_balance.u128()) +// } + +// #[test] +// fn test_native_failed_always_refund() { +// test_native_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::Always, +// RefundReceiver::Proposer, +// ) +// } +// #[test] +// fn test_cw20_failed_always_refund() { +// test_cw20_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::Always, +// RefundReceiver::Proposer, +// ) +// } + +// #[test] +// fn test_native_passed_always_refund() { +// test_native_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::Always, +// RefundReceiver::Proposer, +// ) +// } + +// #[test] +// fn test_cw20_passed_always_refund() { +// test_cw20_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::Always, +// RefundReceiver::Proposer, +// ) +// } + +// #[test] +// fn test_native_passed_never_refund() { +// test_native_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::Never, +// RefundReceiver::Dao, +// ) +// } +// #[test] +// fn test_cw20_passed_never_refund() { +// test_cw20_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::Never, +// RefundReceiver::Dao, +// ) +// } + +// #[test] +// fn test_native_failed_never_refund() { +// test_native_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::Never, +// RefundReceiver::Dao, +// ) +// } +// #[test] +// fn test_cw20_failed_never_refund() { +// test_cw20_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::Never, +// RefundReceiver::Dao, +// ) +// } + +// #[test] +// fn test_native_passed_passed_refund() { +// test_native_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::OnlyPassed, +// RefundReceiver::Proposer, +// ) +// } +// #[test] +// fn test_cw20_passed_passed_refund() { +// test_cw20_permutation( +// EndStatus::Passed, +// DepositRefundPolicy::OnlyPassed, +// RefundReceiver::Proposer, +// ) +// } + +// #[test] +// fn test_native_failed_passed_refund() { +// test_native_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::OnlyPassed, +// RefundReceiver::Dao, +// ) +// } +// #[test] +// fn test_cw20_failed_passed_refund() { +// test_cw20_permutation( +// EndStatus::Failed, +// DepositRefundPolicy::OnlyPassed, +// RefundReceiver::Dao, +// ) +// } // See: #[test] @@ -670,7 +732,6 @@ fn test_multiple_open_proposals() { let mut app = App::default(); let DefaultTestSetup { - core_addr: _, proposal_single, pre_propose, } = setup_default_test( @@ -685,677 +746,677 @@ fn test_multiple_open_proposals() { false, ); - mint_natives(&mut app, "ekez", coins(20, "ujuno")); - let first_id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), - ); - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(10, balance.u128()); - - let second_id = make_proposal( - &mut app, - pre_propose, - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), - ); - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(0, balance.u128()); - - // Finish up the first proposal. - let new_status = vote( - &mut app, - proposal_single.clone(), - "ekez", - first_id, - Vote::Yes, - ); - assert_eq!(Status::Passed, new_status); - - // Still zero. - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(0, balance.u128()); - - execute_proposal(&mut app, proposal_single.clone(), "ekez", first_id); - - // First proposal refunded. - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(10, balance.u128()); - - // Finish up the second proposal. - let new_status = vote( - &mut app, - proposal_single.clone(), - "ekez", - second_id, - Vote::No, - ); - assert_eq!(Status::Rejected, new_status); + // mint_natives(&mut app, "ekez", coins(20, "ujuno")); + // let first_id = make_proposal( + // &mut app, + // pre_propose.clone(), + // proposal_single.clone(), + // "ekez", + // &coins(10, "ujuno"), + // ); + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(10, balance.u128()); + + // let second_id = make_proposal( + // &mut app, + // pre_propose, + // proposal_single.clone(), + // "ekez", + // &coins(10, "ujuno"), + // ); + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(0, balance.u128()); + + // // Finish up the first proposal. + // let new_status = vote( + // &mut app, + // proposal_single.clone(), + // "ekez", + // first_id, + // Vote::Yes, + // ); + // assert_eq!(Status::Passed, new_status); + + // // Still zero. + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(0, balance.u128()); + + // execute_proposal(&mut app, proposal_single.clone(), "ekez", first_id); + + // // First proposal refunded. + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(10, balance.u128()); + + // // Finish up the second proposal. + // let new_status = vote( + // &mut app, + // proposal_single.clone(), + // "ekez", + // second_id, + // Vote::No, + // ); + // assert_eq!(Status::Rejected, new_status); + + // // Still zero. + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(10, balance.u128()); + + // close_proposal(&mut app, proposal_single, "ekez", second_id); + + // // All deposits have been refunded. + // let balance = get_balance_native(&app, "ekez", "ujuno"); + // assert_eq!(20, balance.u128()); +} - // Still zero. - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(10, balance.u128()); - - close_proposal(&mut app, proposal_single, "ekez", second_id); - - // All deposits have been refunded. - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(20, balance.u128()); -} - -#[test] -fn test_set_version() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr: _, - proposal_single: _, - pre_propose, - } = setup_default_test( - &mut app, - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); - - let info: ContractVersion = from_json( - app.wrap() - .query_wasm_raw(pre_propose, "contract_info".as_bytes()) - .unwrap() - .unwrap(), - ) - .unwrap(); - assert_eq!( - ContractVersion { - contract: CONTRACT_NAME.to_string(), - version: CONTRACT_VERSION.to_string() - }, - info - ) -} - -#[test] -fn test_permissions() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single: _, - pre_propose, - } = setup_default_test( - &mut app, - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, // no open proposal submission. - ); - - let err: PreProposeError = app - .execute_contract( - core_addr, - pre_propose.clone(), - &ExecuteMsg::ProposalCompletedHook { - proposal_id: 1, - new_status: Status::Closed, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, PreProposeError::NotModule {}); - - // Non-members may not propose when open_propose_submission is - // disabled. - let err: PreProposeError = app - .execute_contract( - Addr::unchecked("nonmember"), - pre_propose, - &ExecuteMsg::Propose { - msg: ProposeMessage::Propose { - title: "I would like to join the DAO".to_string(), - description: "though, I am currently not a member.".to_string(), - msgs: vec![], - }, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}) -} - -#[test] -fn test_propose_open_proposal_submission() { - let mut app = App::default(); - let DefaultTestSetup { - core_addr: _, - proposal_single, - pre_propose, - } = setup_default_test( - &mut app, - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - true, // yes, open proposal submission. - ); - - // Non-member proposes. - mint_natives(&mut app, "nonmember", coins(10, "ujuno")); - let id = make_proposal( - &mut app, - pre_propose, - proposal_single.clone(), - "nonmember", - &coins(10, "ujuno"), - ); - // Member votes. - let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); - assert_eq!(Status::Passed, new_status) -} - -#[test] -fn test_no_deposit_required_open_submission() { - let mut app = App::default(); - let DefaultTestSetup { - core_addr: _, - proposal_single, - pre_propose, - } = setup_default_test( - &mut app, None, true, // yes, open proposal submission. - ); - - // Non-member proposes. - let id = make_proposal( - &mut app, - pre_propose, - proposal_single.clone(), - "nonmember", - &[], - ); - // Member votes. - let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); - assert_eq!(Status::Passed, new_status) -} - -#[test] -fn test_no_deposit_required_members_submission() { - let mut app = App::default(); - let DefaultTestSetup { - core_addr: _, - proposal_single, - pre_propose, - } = setup_default_test( - &mut app, None, false, // no open proposal submission. - ); - - // Non-member proposes and this fails. - let err: PreProposeError = app - .execute_contract( - Addr::unchecked("nonmember"), - pre_propose.clone(), - &ExecuteMsg::Propose { - msg: ProposeMessage::Propose { - title: "I would like to join the DAO".to_string(), - description: "though, I am currently not a member.".to_string(), - msgs: vec![], - }, - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); - - let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); - let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); - assert_eq!(Status::Passed, new_status) -} - -#[test] -fn test_execute_extension_does_nothing() { - let mut app = App::default(); - let DefaultTestSetup { - core_addr: _, - proposal_single: _, - pre_propose, - } = setup_default_test( - &mut app, None, false, // no open proposal submission. - ); - - let res = app - .execute_contract( - Addr::unchecked("ekez"), - pre_propose, - &ExecuteMsg::Extension { - msg: Empty::default(), - }, - &[], - ) - .unwrap(); - - // There should be one event which is the invocation of the contract. - assert_eq!(res.events.len(), 1); - assert_eq!(res.events[0].ty, "execute".to_string()); - assert_eq!(res.events[0].attributes.len(), 1); - assert_eq!( - res.events[0].attributes[0].key, - "_contract_address".to_string() - ) -} - -#[test] -#[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] -fn test_instantiate_with_zero_native_deposit() { - let mut app = App::default(); - - let dps_id = app.store_code(cw_dao_proposal_single_contract()); - - let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - - dps::msg::InstantiateMsg { - threshold: Threshold::AbsolutePercentage { - percentage: PercentageThreshold::Majority {}, - }, - max_voting_period: Duration::Time(86400), - min_voting_period: None, - only_members_execute: false, - allow_revoting: false, - pre_propose_info: PreProposeInfo::ModuleMayPropose { - info: ModuleInstantiateInfo { - code_id: pre_propose_id, - msg: to_json_binary(&InstantiateMsg { - deposit_info: Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::zero(), - refund_policy: DepositRefundPolicy::OnlyPassed, - }), - open_proposal_submission: false, - extension: Empty::default(), - }) - .unwrap(), - admin: Some(Admin::CoreModule {}), - funds: vec![], - label: "baby's first pre-propose module".to_string(), - }, - }, - close_proposal_on_execution_failure: false, - veto: None, - } - }; - - // Should panic. - instantiate_with_cw4_groups_governance( - &mut app, - dps_id, - to_json_binary(&proposal_module_instantiate).unwrap(), - Some(vec![ - cw20::Cw20Coin { - address: "ekez".to_string(), - amount: Uint128::new(9), - }, - cw20::Cw20Coin { - address: "keze".to_string(), - amount: Uint128::new(8), - }, - ]), - ); -} - -#[test] -#[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] -fn test_instantiate_with_zero_cw20_deposit() { - let mut app = App::default(); - - let cw20_addr = instantiate_cw20_base_default(&mut app); - - let dps_id = app.store_code(cw_dao_proposal_single_contract()); - - let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - - dps::msg::InstantiateMsg { - threshold: Threshold::AbsolutePercentage { - percentage: PercentageThreshold::Majority {}, - }, - max_voting_period: Duration::Time(86400), - min_voting_period: None, - only_members_execute: false, - allow_revoting: false, - pre_propose_info: PreProposeInfo::ModuleMayPropose { - info: ModuleInstantiateInfo { - code_id: pre_propose_id, - msg: to_json_binary(&InstantiateMsg { - deposit_info: Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Cw20(cw20_addr.into_string()), - }, - amount: Uint128::zero(), - refund_policy: DepositRefundPolicy::OnlyPassed, - }), - open_proposal_submission: false, - extension: Empty::default(), - }) - .unwrap(), - admin: Some(Admin::CoreModule {}), - funds: vec![], - label: "baby's first pre-propose module".to_string(), - }, - }, - close_proposal_on_execution_failure: false, - veto: None, - } - }; - - // Should panic. - instantiate_with_cw4_groups_governance( - &mut app, - dps_id, - to_json_binary(&proposal_module_instantiate).unwrap(), - Some(vec![ - cw20::Cw20Coin { - address: "ekez".to_string(), - amount: Uint128::new(9), - }, - cw20::Cw20Coin { - address: "keze".to_string(), - amount: Uint128::new(8), - }, - ]), - ); -} - -#[test] -fn test_update_config() { - let mut app = App::default(); - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test(&mut app, None, false); - - let config = get_config(&app, pre_propose.clone()); - assert_eq!( - config, - Config { - deposit_info: None, - open_proposal_submission: false - } - ); - - let id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &[], - ); - - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Never, - }), - true, - ); - - let config = get_config(&app, pre_propose.clone()); - assert_eq!( - config, - Config { - deposit_info: Some(CheckedDepositInfo { - denom: cw_denom::CheckedDenom::Native("ujuno".to_string()), - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Never - }), - open_proposal_submission: true, - } - ); - - // Old proposal should still have same deposit info. - let info = get_deposit_info(&app, pre_propose.clone(), id); - assert_eq!( - info, - DepositInfoResponse { - deposit_info: None, - proposer: Addr::unchecked("ekez"), - } - ); - - // New proposals should have the new deposit info. - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let new_id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), - ); - let info = get_deposit_info(&app, pre_propose.clone(), new_id); - assert_eq!( - info, - DepositInfoResponse { - deposit_info: Some(CheckedDepositInfo { - denom: cw_denom::CheckedDenom::Native("ujuno".to_string()), - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Never - }), - proposer: Addr::unchecked("ekez"), - } - ); - - // Both proposals should be allowed to complete. - vote(&mut app, proposal_single.clone(), "ekez", id, Vote::Yes); - vote(&mut app, proposal_single.clone(), "ekez", new_id, Vote::Yes); - execute_proposal(&mut app, proposal_single.clone(), "ekez", id); - execute_proposal(&mut app, proposal_single.clone(), "ekez", new_id); - // Deposit should not have been refunded (never policy in use). - let balance = get_balance_native(&app, "ekez", "ujuno"); - assert_eq!(balance, Uint128::new(0)); - - // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); - assert_eq!(err, PreProposeError::NotDao {}); -} - -#[test] -fn test_withdraw() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test(&mut app, None, false); - - let err = withdraw_should_fail( - &mut app, - pre_propose.clone(), - proposal_single.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), - ); - assert_eq!(err, PreProposeError::NotDao {}); - - let err = withdraw_should_fail( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), - ); - assert_eq!(err, PreProposeError::NothingToWithdraw {}); - - let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); - assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); - - // Turn on native deposits. - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); - - // Withdraw with no specified denom - should fall back to the one - // in the config. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(10)); - - // Withdraw again, this time specifying a native denomination. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), - ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(20)); - - // Make a proposal with the native tokens to put some in the system. - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let native_id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), - ); - - // Update the config to use a cw20 token. - let cw20_address = instantiate_cw20_base_default(&mut app); - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Cw20(cw20_address.to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); - - increase_allowance( - &mut app, - "ekez", - &pre_propose, - cw20_address.clone(), - Uint128::new(10), - ); - let cw20_id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &[], - ); - - // There is now a pending proposal and cw20 tokens in the - // pre-propose module that should be returned on that proposal's - // completion. To make things interesting, we withdraw those - // tokens which should cause the status change hook on the - // proposal's execution to fail as we don't have sufficent balance - // to return the deposit. - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); - assert_eq!(balance, Uint128::new(10)); - - // Proposal should still be executable! We just get removed from - // the proposal module's hook receiver list. - vote( - &mut app, - proposal_single.clone(), - "ekez", - cw20_id, - Vote::Yes, - ); - execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); - - // Make sure the proposal module has fallen back to anyone can - // propose becuase of our malfunction. - let proposal_creation_policy: ProposalCreationPolicy = app - .wrap() - .query_wasm_smart( - proposal_single.clone(), - &dps::msg::QueryMsg::ProposalCreationPolicy {}, - ) - .unwrap(); - - assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); - - // Close out the native proposal and it's deposit as well. - vote( - &mut app, - proposal_single.clone(), - "ekez", - native_id, - Vote::No, - ); - close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); - withdraw( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), - ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(30)); -} - -#[test] -fn test_hook_management() { - let app = &mut App::default(); - let DefaultTestSetup { - core_addr, - proposal_single: _, - pre_propose, - } = setup_default_test(app, None, true); - - add_hook(app, core_addr.as_str(), &pre_propose, "one"); - add_hook(app, core_addr.as_str(), &pre_propose, "two"); - - remove_hook(app, core_addr.as_str(), &pre_propose, "one"); - - let hooks = query_hooks(app, pre_propose).hooks; - assert_eq!(hooks, vec!["two".to_string()]) -} +// #[test] +// fn test_set_version() { +// let mut app = App::default(); + +// let DefaultTestSetup { +// core_addr: _, +// proposal_single: _, +// pre_propose, +// } = setup_default_test( +// &mut app, +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Always, +// }), +// false, +// ); + +// let info: ContractVersion = from_binary( +// app.wrap() +// .query_wasm_raw(pre_propose, "contract_info".as_bytes()) +// .unwrap() +// .unwrap(), +// ) +// .unwrap(); +// assert_eq!( +// ContractVersion { +// contract: CONTRACT_NAME.to_string(), +// version: CONTRACT_VERSION.to_string() +// }, +// info +// ) +// } + +// #[test] +// fn test_permissions() { +// let mut app = App::default(); + +// let DefaultTestSetup { +// core_addr, +// proposal_single: _, +// pre_propose, +// } = setup_default_test( +// &mut app, +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Always, +// }), +// false, // no open proposal submission. +// ); + +// let err: PreProposeError = app +// .execute_contract( +// core_addr, +// pre_propose.clone(), +// &ExecuteMsg::ProposalCompletedHook { +// proposal_id: 1, +// new_status: Status::Closed, +// }, +// &[], +// ) +// .unwrap_err() +// .downcast() +// .unwrap(); +// assert_eq!(err, PreProposeError::NotModule {}); + +// // Non-members may not propose when open_propose_submission is +// // disabled. +// let err: PreProposeError = app +// .execute_contract( +// Addr::unchecked("nonmember"), +// pre_propose, +// &ExecuteMsg::Propose { +// msg: ProposeMessage::Propose { +// title: "I would like to join the DAO".to_string(), +// description: "though, I am currently not a member.".to_string(), +// msgs: vec![], +// }, +// }, +// &[], +// ) +// .unwrap_err() +// .downcast() +// .unwrap(); +// assert_eq!(err, PreProposeError::NotMember {}) +// } + +// #[test] +// fn test_propose_open_proposal_submission() { +// let mut app = App::default(); +// let DefaultTestSetup { +// core_addr: _, +// proposal_single, +// pre_propose, +// } = setup_default_test( +// &mut app, +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Always, +// }), +// true, // yes, open proposal submission. +// ); + +// // Non-member proposes. +// mint_natives(&mut app, "nonmember", coins(10, "ujuno")); +// let id = make_proposal( +// &mut app, +// pre_propose, +// proposal_single.clone(), +// "nonmember", +// &coins(10, "ujuno"), +// ); +// // Member votes. +// let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); +// assert_eq!(Status::Passed, new_status) +// } + +// #[test] +// fn test_no_deposit_required_open_submission() { +// let mut app = App::default(); +// let DefaultTestSetup { +// core_addr: _, +// proposal_single, +// pre_propose, +// } = setup_default_test( +// &mut app, None, true, // yes, open proposal submission. +// ); + +// // Non-member proposes. +// let id = make_proposal( +// &mut app, +// pre_propose, +// proposal_single.clone(), +// "nonmember", +// &[], +// ); +// // Member votes. +// let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); +// assert_eq!(Status::Passed, new_status) +// } + +// #[test] +// fn test_no_deposit_required_members_submission() { +// let mut app = App::default(); +// let DefaultTestSetup { +// core_addr: _, +// proposal_single, +// pre_propose, +// } = setup_default_test( +// &mut app, None, false, // no open proposal submission. +// ); + +// // Non-member proposes and this fails. +// let err: PreProposeError = app +// .execute_contract( +// Addr::unchecked("nonmember"), +// pre_propose.clone(), +// &ExecuteMsg::Propose { +// msg: ProposeMessage::Propose { +// title: "I would like to join the DAO".to_string(), +// description: "though, I am currently not a member.".to_string(), +// msgs: vec![], +// }, +// }, +// &[], +// ) +// .unwrap_err() +// .downcast() +// .unwrap(); +// assert_eq!(err, PreProposeError::NotMember {}); + +// let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); +// let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); +// assert_eq!(Status::Passed, new_status) +// } + +// #[test] +// fn test_execute_extension_does_nothing() { +// let mut app = App::default(); +// let DefaultTestSetup { +// core_addr: _, +// proposal_single: _, +// pre_propose, +// } = setup_default_test( +// &mut app, None, false, // no open proposal submission. +// ); + +// let res = app +// .execute_contract( +// Addr::unchecked("ekez"), +// pre_propose, +// &ExecuteMsg::Extension { +// msg: Empty::default(), +// }, +// &[], +// ) +// .unwrap(); + +// // There should be one event which is the invocation of the contract. +// assert_eq!(res.events.len(), 1); +// assert_eq!(res.events[0].ty, "execute".to_string()); +// assert_eq!(res.events[0].attributes.len(), 1); +// assert_eq!( +// res.events[0].attributes[0].key, +// "_contract_address".to_string() +// ) +// } + +// #[test] +// #[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] +// fn test_instantiate_with_zero_native_deposit() { +// let mut app = App::default(); + +// let dps_id = app.store_code(cw_dao_proposal_single_contract()); + +// let proposal_module_instantiate = { +// let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + +// dps::msg::InstantiateMsg { +// threshold: Threshold::AbsolutePercentage { +// percentage: PercentageThreshold::Majority {}, +// }, +// max_voting_period: Duration::Time(86400), +// min_voting_period: None, +// only_members_execute: false, +// allow_revoting: false, +// pre_propose_info: PreProposeInfo::ModuleMayPropose { +// info: ModuleInstantiateInfo { +// code_id: pre_propose_id, +// msg: to_binary(&InstantiateMsg { +// deposit_info: Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::zero(), +// refund_policy: DepositRefundPolicy::OnlyPassed, +// }), +// open_proposal_submission: false, +// extension: Empty::default(), +// }) +// .unwrap(), +// admin: Some(Admin::CoreModule {}), +// funds: vec![], +// label: "baby's first pre-propose module".to_string(), +// }, +// }, +// close_proposal_on_execution_failure: false, +// veto: None, +// } +// }; + +// // Should panic. +// instantiate_with_cw4_groups_governance( +// &mut app, +// dps_id, +// to_binary(&proposal_module_instantiate).unwrap(), +// Some(vec![ +// cw20::Cw20Coin { +// address: "ekez".to_string(), +// amount: Uint128::new(9), +// }, +// cw20::Cw20Coin { +// address: "keze".to_string(), +// amount: Uint128::new(8), +// }, +// ]), +// ); +// } + +// #[test] +// #[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] +// fn test_instantiate_with_zero_cw20_deposit() { +// let mut app = App::default(); + +// let cw20_addr = instantiate_cw20_base_default(&mut app); + +// let dps_id = app.store_code(cw_dao_proposal_single_contract()); + +// let proposal_module_instantiate = { +// let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + +// dps::msg::InstantiateMsg { +// threshold: Threshold::AbsolutePercentage { +// percentage: PercentageThreshold::Majority {}, +// }, +// max_voting_period: Duration::Time(86400), +// min_voting_period: None, +// only_members_execute: false, +// allow_revoting: false, +// pre_propose_info: PreProposeInfo::ModuleMayPropose { +// info: ModuleInstantiateInfo { +// code_id: pre_propose_id, +// msg: to_binary(&InstantiateMsg { +// deposit_info: Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Cw20(cw20_addr.into_string()), +// }, +// amount: Uint128::zero(), +// refund_policy: DepositRefundPolicy::OnlyPassed, +// }), +// open_proposal_submission: false, +// extension: Empty::default(), +// }) +// .unwrap(), +// admin: Some(Admin::CoreModule {}), +// funds: vec![], +// label: "baby's first pre-propose module".to_string(), +// }, +// }, +// close_proposal_on_execution_failure: false, +// veto: None, +// } +// }; + +// // Should panic. +// instantiate_with_cw4_groups_governance( +// &mut app, +// dps_id, +// to_binary(&proposal_module_instantiate).unwrap(), +// Some(vec![ +// cw20::Cw20Coin { +// address: "ekez".to_string(), +// amount: Uint128::new(9), +// }, +// cw20::Cw20Coin { +// address: "keze".to_string(), +// amount: Uint128::new(8), +// }, +// ]), +// ); +// } + +// #[test] +// fn test_update_config() { +// let mut app = App::default(); +// let DefaultTestSetup { +// core_addr, +// proposal_single, +// pre_propose, +// } = setup_default_test(&mut app, None, false); + +// let config = get_config(&app, pre_propose.clone()); +// assert_eq!( +// config, +// Config { +// deposit_info: None, +// open_proposal_submission: false +// } +// ); + +// let id = make_proposal( +// &mut app, +// pre_propose.clone(), +// proposal_single.clone(), +// "ekez", +// &[], +// ); + +// update_config( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Never, +// }), +// true, +// ); + +// let config = get_config(&app, pre_propose.clone()); +// assert_eq!( +// config, +// Config { +// deposit_info: Some(CheckedDepositInfo { +// denom: cw_denom::CheckedDenom::Native("ujuno".to_string()), +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Never +// }), +// open_proposal_submission: true, +// } +// ); + +// // Old proposal should still have same deposit info. +// let info = get_deposit_info(&app, pre_propose.clone(), id); +// assert_eq!( +// info, +// DepositInfoResponse { +// deposit_info: None, +// proposer: Addr::unchecked("ekez"), +// } +// ); + +// // New proposals should have the new deposit info. +// mint_natives(&mut app, "ekez", coins(10, "ujuno")); +// let new_id = make_proposal( +// &mut app, +// pre_propose.clone(), +// proposal_single.clone(), +// "ekez", +// &coins(10, "ujuno"), +// ); +// let info = get_deposit_info(&app, pre_propose.clone(), new_id); +// assert_eq!( +// info, +// DepositInfoResponse { +// deposit_info: Some(CheckedDepositInfo { +// denom: cw_denom::CheckedDenom::Native("ujuno".to_string()), +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Never +// }), +// proposer: Addr::unchecked("ekez"), +// } +// ); + +// // Both proposals should be allowed to complete. +// vote(&mut app, proposal_single.clone(), "ekez", id, Vote::Yes); +// vote(&mut app, proposal_single.clone(), "ekez", new_id, Vote::Yes); +// execute_proposal(&mut app, proposal_single.clone(), "ekez", id); +// execute_proposal(&mut app, proposal_single.clone(), "ekez", new_id); +// // Deposit should not have been refunded (never policy in use). +// let balance = get_balance_native(&app, "ekez", "ujuno"); +// assert_eq!(balance, Uint128::new(0)); + +// // Only the core module can update the config. +// let err = +// update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); +// assert_eq!(err, PreProposeError::NotDao {}); +// } + +// #[test] +// fn test_withdraw() { +// let mut app = App::default(); + +// let DefaultTestSetup { +// core_addr, +// proposal_single, +// pre_propose, +// } = setup_default_test(&mut app, None, false); + +// let err = withdraw_should_fail( +// &mut app, +// pre_propose.clone(), +// proposal_single.as_str(), +// Some(UncheckedDenom::Native("ujuno".to_string())), +// ); +// assert_eq!(err, PreProposeError::NotDao {}); + +// let err = withdraw_should_fail( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDenom::Native("ujuno".to_string())), +// ); +// assert_eq!(err, PreProposeError::NothingToWithdraw {}); + +// let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); +// assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); + +// // Turn on native deposits. +// update_config( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Native("ujuno".to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Always, +// }), +// false, +// ); + +// // Withdraw with no specified denom - should fall back to the one +// // in the config. +// mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); +// withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); +// let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); +// assert_eq!(balance, Uint128::new(10)); + +// // Withdraw again, this time specifying a native denomination. +// mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); +// withdraw( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDenom::Native("ujuno".to_string())), +// ); +// let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); +// assert_eq!(balance, Uint128::new(20)); + +// // Make a proposal with the native tokens to put some in the system. +// mint_natives(&mut app, "ekez", coins(10, "ujuno")); +// let native_id = make_proposal( +// &mut app, +// pre_propose.clone(), +// proposal_single.clone(), +// "ekez", +// &coins(10, "ujuno"), +// ); + +// // Update the config to use a cw20 token. +// let cw20_address = instantiate_cw20_base_default(&mut app); +// update_config( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDepositInfo { +// denom: DepositToken::Token { +// denom: UncheckedDenom::Cw20(cw20_address.to_string()), +// }, +// amount: Uint128::new(10), +// refund_policy: DepositRefundPolicy::Always, +// }), +// false, +// ); + +// increase_allowance( +// &mut app, +// "ekez", +// &pre_propose, +// cw20_address.clone(), +// Uint128::new(10), +// ); +// let cw20_id = make_proposal( +// &mut app, +// pre_propose.clone(), +// proposal_single.clone(), +// "ekez", +// &[], +// ); + +// // There is now a pending proposal and cw20 tokens in the +// // pre-propose module that should be returned on that proposal's +// // completion. To make things interesting, we withdraw those +// // tokens which should cause the status change hook on the +// // proposal's execution to fail as we don't have sufficent balance +// // to return the deposit. +// withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); +// let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); +// assert_eq!(balance, Uint128::new(10)); + +// // Proposal should still be executable! We just get removed from +// // the proposal module's hook receiver list. +// vote( +// &mut app, +// proposal_single.clone(), +// "ekez", +// cw20_id, +// Vote::Yes, +// ); +// execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + +// // Make sure the proposal module has fallen back to anyone can +// // propose becuase of our malfunction. +// let proposal_creation_policy: ProposalCreationPolicy = app +// .wrap() +// .query_wasm_smart( +// proposal_single.clone(), +// &dps::msg::QueryMsg::ProposalCreationPolicy {}, +// ) +// .unwrap(); + +// assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + +// // Close out the native proposal and it's deposit as well. +// vote( +// &mut app, +// proposal_single.clone(), +// "ekez", +// native_id, +// Vote::No, +// ); +// close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); +// withdraw( +// &mut app, +// pre_propose.clone(), +// core_addr.as_str(), +// Some(UncheckedDenom::Native("ujuno".to_string())), +// ); +// let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); +// assert_eq!(balance, Uint128::new(30)); +// } + +// #[test] +// fn test_hook_management() { +// let app = &mut App::default(); +// let DefaultTestSetup { +// core_addr, +// proposal_single: _, +// pre_propose, +// } = setup_default_test(app, None, true); + +// add_hook(app, core_addr.as_str(), &pre_propose, "one"); +// add_hook(app, core_addr.as_str(), &pre_propose, "two"); + +// remove_hook(app, core_addr.as_str(), &pre_propose, "one"); + +// let hooks = query_hooks(app, pre_propose).hooks; +// assert_eq!(hooks, vec!["two".to_string()]) +// } diff --git a/contracts/proposal/dao-proposal-single/Cargo.toml b/contracts/proposal/dao-proposal-single/Cargo.toml index e2c44f7..8cf5fd6 100644 --- a/contracts/proposal/dao-proposal-single/Cargo.toml +++ b/contracts/proposal/dao-proposal-single/Cargo.toml @@ -38,7 +38,6 @@ schemars ={ workspace = true } secret-cw-controllers ={ workspace = true } shade-protocol ={ workspace = true } query_auth ={ workspace = true } -dao-pre-propose-single = { workspace = true } dao-testing ={ workspace = true } @@ -51,7 +50,6 @@ dao-voting-cw4 = { workspace = true } dao-voting-snip20-staked = { workspace = true } dao-voting-token-staked = { workspace = true } dao-voting-snip721-staked = { workspace = true } -dao-pre-propose-single = { workspace = true } cw-denom = { workspace = true } snip20-stake = { workspace = true } snip20-reference-impl = { workspace = true } diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index 4e67789..44e810a 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -9,9 +9,11 @@ "required": [ "allow_revoting", "close_proposal_on_execution_failure", + "dao_code_hash", "max_voting_period", "only_members_execute", "pre_propose_info", + "query_auth", "threshold" ], "properties": { @@ -23,6 +25,9 @@ "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", "type": "boolean" }, + "dao_code_hash": { + "type": "string" + }, "max_voting_period": { "description": "The default maximum amount of time a proposal may be voted on before expiring.", "allOf": [ @@ -54,6 +59,9 @@ } ] }, + "query_auth": { + "$ref": "#/definitions/RawContract" + }, "threshold": { "description": "The threshold a proposal must reach to complete.", "allOf": [ @@ -74,7 +82,6 @@ ] } }, - "additionalProperties": false, "definitions": { "Admin": { "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", @@ -95,8 +102,7 @@ "addr": { "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -109,8 +115,7 @@ ], "properties": { "core_module": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -178,6 +183,7 @@ "description": "Information needed to instantiate a module.", "type": "object", "required": [ + "code_hash", "code_id", "funds", "label", @@ -195,6 +201,10 @@ } ] }, + "code_hash": { + "description": "Code Hash of the contract to be instantiated.", + "type": "string" + }, "code_id": { "description": "Code ID of the contract to be instantiated.", "type": "integer", @@ -220,8 +230,7 @@ } ] } - }, - "additionalProperties": false + } }, "PercentageThreshold": { "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", @@ -234,8 +243,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -265,8 +273,7 @@ ], "properties": { "anyone_may_propose": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -287,14 +294,30 @@ "info": { "$ref": "#/definitions/ModuleInstantiateInfo" } - }, - "additionalProperties": false + } } }, "additionalProperties": false } ] }, + "RawContract": { + "description": "A contract that does not contain a validated address. Should be accepted as user input because we shouldn't assume addresses are verified Addrs. https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Addr.html", + "type": "object", + "required": [ + "address", + "code_hash" + ], + "properties": { + "address": { + "type": "string" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + }, "Threshold": { "description": "The ways a proposal may reach its passing / failing threshold.", "oneOf": [ @@ -314,8 +337,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -340,8 +362,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -362,8 +383,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -371,7 +391,7 @@ ] }, "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); ```", + "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 secret_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" }, "VetoConfig": { @@ -403,8 +423,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } } } }, @@ -435,10 +454,14 @@ "vote": { "type": "object", "required": [ + "auth", "proposal_id", "vote" ], "properties": { + "auth": { + "$ref": "#/definitions/Auth" + }, "proposal_id": { "description": "The ID of the proposal to vote on.", "type": "integer", @@ -460,8 +483,7 @@ } ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -490,8 +512,7 @@ "null" ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -506,17 +527,20 @@ "execute": { "type": "object", "required": [ + "auth", "proposal_id" ], "properties": { + "auth": { + "$ref": "#/definitions/Auth" + }, "proposal_id": { "description": "The ID of the proposal to execute.", "type": "integer", "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -540,8 +564,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -565,8 +588,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -583,9 +605,11 @@ "required": [ "allow_revoting", "close_proposal_on_execution_failure", + "code_hash", "dao", "max_voting_period", "only_members_execute", + "query_auth", "threshold" ], "properties": { @@ -597,6 +621,9 @@ "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", "type": "boolean" }, + "code_hash": { + "type": "string" + }, "dao": { "description": "The address if tge DAO that this governance module is associated with.", "type": "string" @@ -624,6 +651,9 @@ "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal. Applies to all outstanding and future proposals.", "type": "boolean" }, + "query_auth": { + "$ref": "#/definitions/RawContract" + }, "threshold": { "description": "The new proposal passing threshold. This will only apply to proposals created after the config update.", "allOf": [ @@ -643,8 +673,7 @@ } ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -665,8 +694,7 @@ "info": { "$ref": "#/definitions/PreProposeInfo" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -681,14 +709,17 @@ "add_proposal_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -703,14 +734,17 @@ "remove_proposal_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -725,14 +759,17 @@ "add_vote_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -747,14 +784,17 @@ "remove_vote_hook": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -780,8 +820,7 @@ "addr": { "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -794,11 +833,51 @@ ], "properties": { "core_module": { + "type": "object" + } + }, + "additionalProperties": false + } + ] + }, + "Auth": { + "oneOf": [ + { + "type": "object", + "required": [ + "viewing_key" + ], + "properties": { + "viewing_key": { "type": "object", + "required": [ + "address", + "key" + ], + "properties": { + "address": { + "type": "string" + }, + "key": { + "type": "string" + } + }, "additionalProperties": false } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permit" + ], + "properties": { + "permit": { + "$ref": "#/definitions/Permit_for_PermitData" + } + }, + "additionalProperties": false } ] }, @@ -943,6 +1022,7 @@ ], "properties": { "type_url": { + "description": "this is the fully qualified msg path used for routing, e.g. /cosmos.bank.v1beta1.MsgSend NOTE: the type_url can be changed after a chain upgrade", "type": "string" }, "value": { @@ -988,6 +1068,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finalize_tx" + ], + "properties": { + "finalize_tx": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false } ] }, @@ -1083,7 +1175,6 @@ "type": "object" }, "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ { "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", @@ -1105,12 +1196,7 @@ "minimum": 0.0 }, "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] + "$ref": "#/definitions/VoteOption" } } } @@ -1134,6 +1220,7 @@ "required": [ "amount", "channel_id", + "memo", "timeout", "to_address" ], @@ -1147,7 +1234,11 @@ ] }, "channel_id": { - "description": "existing channel to send the tokens over", + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "memo": { + "description": "optional memo can put here `{\"ibc_callback\":\"secret1contractAddr\"}` to get a callback on ack/timeout see this for more info: https://github.com/scrtlabs/SecretNetwork/blob/78a5f82a4/x/ibc-hooks/README.md?plain=1#L144-L188", "type": "string" }, "timeout": { @@ -1265,7 +1356,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1276,6 +1367,7 @@ "description": "Information needed to instantiate a module.", "type": "object", "required": [ + "code_hash", "code_id", "funds", "label", @@ -1293,6 +1385,10 @@ } ] }, + "code_hash": { + "description": "Code Hash of the contract to be instantiated.", + "type": "string" + }, "code_id": { "description": "Code ID of the contract to be instantiated.", "type": "integer", @@ -1318,8 +1414,7 @@ } ] } - }, - "additionalProperties": false + } }, "PercentageThreshold": { "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", @@ -1332,8 +1427,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1353,6 +1447,87 @@ } ] }, + "PermitData": { + "type": "object", + "required": [ + "data", + "key" + ], + "properties": { + "data": { + "$ref": "#/definitions/Binary" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PermitSignature": { + "type": "object", + "required": [ + "pub_key", + "signature" + ], + "properties": { + "pub_key": { + "$ref": "#/definitions/PubKey" + }, + "signature": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "Permit_for_PermitData": { + "description": "Where the information will be stored", + "type": "object", + "required": [ + "params", + "signature" + ], + "properties": { + "account_number": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "chain_id": { + "type": [ + "string", + "null" + ] + }, + "memo": { + "type": [ + "string", + "null" + ] + }, + "params": { + "$ref": "#/definitions/PermitData" + }, + "sequence": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "signature": { + "$ref": "#/definitions/PermitSignature" + } + }, + "additionalProperties": false + }, "PreProposeInfo": { "oneOf": [ { @@ -1363,8 +1538,7 @@ ], "properties": { "anyone_may_propose": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1385,14 +1559,52 @@ "info": { "$ref": "#/definitions/ModuleInstantiateInfo" } - }, - "additionalProperties": false + } } }, "additionalProperties": false } ] }, + "PubKey": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "description": "ignored, but must be \"tendermint/PubKeySecp256k1\" otherwise the verification will fail", + "type": "string" + }, + "value": { + "description": "Secp256k1 PubKey", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "RawContract": { + "description": "A contract that does not contain a validated address. Should be accepted as user input because we shouldn't assume addresses are verified Addrs. https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Addr.html", + "type": "object", + "required": [ + "address", + "code_hash" + ], + "properties": { + "address": { + "type": "string" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + }, "SingleChoiceProposeMsg": { "description": "The contents of a message to create a proposal in the single choice proposal module.\n\nWe break this type out of `ExecuteMsg` because we want pre-propose modules that interact with this contract to be able to get type checking on their propose messages.\n\nWe move this type to this package so that pre-propose modules can import it without importing dao-proposal-single with the library feature which (as it is not additive) cause the execute exports to not be included in wasm builds.", "type": "object", @@ -1424,8 +1636,7 @@ "description": "The title of the proposal.", "type": "string" } - }, - "additionalProperties": false + } }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", @@ -1530,8 +1741,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1556,8 +1766,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1578,8 +1787,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1587,7 +1795,7 @@ ] }, "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); ```", + "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 secret_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" @@ -1595,11 +1803,11 @@ ] }, "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); ```", + "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 secret_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); ```", + "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 secret_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" }, "VetoConfig": { @@ -1631,8 +1839,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } }, "Vote": { "oneOf": [ @@ -1681,19 +1888,18 @@ "execute": { "type": "object", "required": [ + "code_hash", "contract_addr", - "funds", - "msg" + "msg", + "send" ], "properties": { - "contract_addr": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", "type": "string" }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "contract_addr": { + "type": "string" }, "msg": { "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", @@ -1702,6 +1908,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -1709,7 +1921,7 @@ "additionalProperties": false }, { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", "type": "object", "required": [ "instantiate" @@ -1718,10 +1930,11 @@ "instantiate": { "type": "object", "required": [ + "code_hash", "code_id", - "funds", "label", - "msg" + "msg", + "send" ], "properties": { "admin": { @@ -1730,19 +1943,17 @@ "null" ] }, + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, "code_id": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, "label": { - "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "description": "A human-readbale label for the contract, must be unique across all contracts", "type": "string" }, "msg": { @@ -1752,6 +1963,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -1768,11 +1985,22 @@ "migrate": { "type": "object", "required": [ + "code_hash", + "code_id", "contract_addr", - "msg", - "new_code_id" + "msg" ], "properties": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the **new** code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, + "code_id": { + "description": "the code_id of the **new** logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "contract_addr": { "type": "string" }, @@ -1783,12 +2011,6 @@ "$ref": "#/definitions/Binary" } ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -1857,8 +2079,7 @@ ], "properties": { "config": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -1881,8 +2102,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1915,8 +2135,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1949,8 +2168,7 @@ "format": "uint64", "minimum": 0.0 } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -1965,20 +2183,19 @@ "get_vote": { "type": "object", "required": [ - "proposal_id", - "voter" + "auth", + "proposal_id" ], "properties": { + "auth": { + "$ref": "#/definitions/Auth" + }, "proposal_id": { "type": "integer", "format": "uint64", "minimum": 0.0 - }, - "voter": { - "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2018,8 +2235,7 @@ "null" ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2032,8 +2248,7 @@ ], "properties": { "proposal_count": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2046,8 +2261,7 @@ ], "properties": { "proposal_creation_policy": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2060,8 +2274,7 @@ ], "properties": { "proposal_hooks": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2074,8 +2287,7 @@ ], "properties": { "vote_hooks": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2088,8 +2300,7 @@ ], "properties": { "dao": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2102,8 +2313,7 @@ ], "properties": { "info": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2116,13 +2326,166 @@ ], "properties": { "next_proposal_id": { + "type": "object" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Auth": { + "oneOf": [ + { + "type": "object", + "required": [ + "viewing_key" + ], + "properties": { + "viewing_key": { + "type": "object", + "required": [ + "address", + "key" + ], + "properties": { + "address": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { "type": "object", + "required": [ + "permit" + ], + "properties": { + "permit": { + "$ref": "#/definitions/Permit_for_PermitData" + } + }, "additionalProperties": false } + ] + }, + "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" + }, + "PermitData": { + "type": "object", + "required": [ + "data", + "key" + ], + "properties": { + "data": { + "$ref": "#/definitions/Binary" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PermitSignature": { + "type": "object", + "required": [ + "pub_key", + "signature" + ], + "properties": { + "pub_key": { + "$ref": "#/definitions/PubKey" + }, + "signature": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "Permit_for_PermitData": { + "description": "Where the information will be stored", + "type": "object", + "required": [ + "params", + "signature" + ], + "properties": { + "account_number": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "chain_id": { + "type": [ + "string", + "null" + ] + }, + "memo": { + "type": [ + "string", + "null" + ] + }, + "params": { + "$ref": "#/definitions/PermitData" + }, + "sequence": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "signature": { + "$ref": "#/definitions/PermitSignature" + } + }, + "additionalProperties": false + }, + "PubKey": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "description": "ignored, but must be \"tendermint/PubKeySecp256k1\" otherwise the verification will fail", + "type": "string" + }, + "value": { + "description": "Secp256k1 PubKey", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } }, "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 secret_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" } - ] + } }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -2164,8 +2527,7 @@ } ] } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2177,8 +2539,7 @@ ], "properties": { "from_compatible": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2204,8 +2565,7 @@ "addr": { "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2218,8 +2578,7 @@ ], "properties": { "core_module": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2283,6 +2642,7 @@ "description": "Information needed to instantiate a module.", "type": "object", "required": [ + "code_hash", "code_id", "funds", "label", @@ -2300,6 +2660,10 @@ } ] }, + "code_hash": { + "description": "Code Hash of the contract to be instantiated.", + "type": "string" + }, "code_id": { "description": "Code ID of the contract to be instantiated.", "type": "integer", @@ -2325,8 +2689,7 @@ } ] } - }, - "additionalProperties": false + } }, "PreProposeInfo": { "oneOf": [ @@ -2338,8 +2701,7 @@ ], "properties": { "anyone_may_propose": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2360,8 +2722,7 @@ "info": { "$ref": "#/definitions/ModuleInstantiateInfo" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2369,7 +2730,7 @@ ] }, "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); ```", + "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 secret_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" }, "VetoConfig": { @@ -2401,8 +2762,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } } } }, @@ -2416,9 +2776,9 @@ "required": [ "allow_revoting", "close_proposal_on_execution_failure", - "dao", "max_voting_period", "only_members_execute", + "query_auth", "threshold" ], "properties": { @@ -2430,14 +2790,6 @@ "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", "type": "boolean" }, - "dao": { - "description": "The address of the DAO that this governance module is associated with.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, "max_voting_period": { "description": "The default maximum amount of time a proposal may be voted on before expiring.", "allOf": [ @@ -2461,6 +2813,9 @@ "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal.", "type": "boolean" }, + "query_auth": { + "$ref": "#/definitions/Contract" + }, "threshold": { "description": "The threshold a proposal must reach to complete.", "allOf": [ @@ -2481,12 +2836,28 @@ ] } }, - "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" }, + "Contract": { + "description": "In the process of being deprecated for [cosmwasm_std::ContractInfo] so use that instead when possible.", + "type": "object", + "required": [ + "address", + "code_hash" + ], + "properties": { + "address": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -2536,8 +2907,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -2576,8 +2946,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2602,8 +2971,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2624,8 +2992,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -2633,7 +3000,7 @@ ] }, "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); ```", + "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 secret_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" }, "VetoConfig": { @@ -2665,8 +3032,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } } } }, @@ -2694,14 +3060,13 @@ ] } }, - "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" }, "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); ```", + "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 secret_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" }, "Vote": { @@ -2769,8 +3134,7 @@ } ] } - }, - "additionalProperties": false + } } } }, @@ -2786,7 +3150,6 @@ "$ref": "#/definitions/ContractVersion" } }, - "additionalProperties": false, "definitions": { "ContractVersion": { "type": "object", @@ -2824,7 +3187,6 @@ } } }, - "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.", @@ -2971,6 +3333,7 @@ ], "properties": { "type_url": { + "description": "this is the fully qualified msg path used for routing, e.g. /cosmos.bank.v1beta1.MsgSend NOTE: the type_url can be changed after a chain upgrade", "type": "string" }, "value": { @@ -3016,6 +3379,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finalize_tx" + ], + "properties": { + "finalize_tx": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false } ] }, @@ -3149,8 +3524,7 @@ ], "properties": { "never": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -3158,7 +3532,6 @@ ] }, "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ { "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", @@ -3180,12 +3553,7 @@ "minimum": 0.0 }, "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] + "$ref": "#/definitions/VoteOption" } } } @@ -3209,6 +3577,7 @@ "required": [ "amount", "channel_id", + "memo", "timeout", "to_address" ], @@ -3222,7 +3591,11 @@ ] }, "channel_id": { - "description": "existing channel to send the tokens over", + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "memo": { + "description": "optional memo can put here `{\"ibc_callback\":\"secret1contractAddr\"}` to get a callback on ack/timeout see this for more info: https://github.com/scrtlabs/SecretNetwork/blob/78a5f82a4/x/ibc-hooks/README.md?plain=1#L144-L188", "type": "string" }, "timeout": { @@ -3340,7 +3713,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -3358,8 +3731,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -3396,8 +3768,7 @@ "proposal": { "$ref": "#/definitions/SingleChoiceProposal" } - }, - "additionalProperties": false + } }, "SingleChoiceProposal": { "type": "object", @@ -3510,8 +3881,7 @@ } ] } - }, - "additionalProperties": false + } }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", @@ -3657,8 +4027,7 @@ "expiration": { "$ref": "#/definitions/Expiration" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -3691,8 +4060,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -3717,8 +4085,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -3739,8 +4106,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -3748,7 +4114,7 @@ ] }, "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); ```", + "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 secret_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" @@ -3756,11 +4122,11 @@ ] }, "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); ```", + "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 secret_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); ```", + "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 secret_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" }, "VetoConfig": { @@ -3792,8 +4158,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } }, "VoteOption": { "type": "string", @@ -3821,8 +4186,7 @@ "yes": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } }, "WasmMsg": { "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", @@ -3837,19 +4201,18 @@ "execute": { "type": "object", "required": [ + "code_hash", "contract_addr", - "funds", - "msg" + "msg", + "send" ], "properties": { - "contract_addr": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", "type": "string" }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "contract_addr": { + "type": "string" }, "msg": { "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", @@ -3858,6 +4221,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -3865,7 +4234,7 @@ "additionalProperties": false }, { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", "type": "object", "required": [ "instantiate" @@ -3874,10 +4243,11 @@ "instantiate": { "type": "object", "required": [ + "code_hash", "code_id", - "funds", "label", - "msg" + "msg", + "send" ], "properties": { "admin": { @@ -3886,19 +4256,17 @@ "null" ] }, + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, "code_id": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, "label": { - "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "description": "A human-readbale label for the contract, must be unique across all contracts", "type": "string" }, "msg": { @@ -3908,6 +4276,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -3924,11 +4298,22 @@ "migrate": { "type": "object", "required": [ + "code_hash", + "code_id", "contract_addr", - "msg", - "new_code_id" + "msg" ], "properties": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the **new** code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, + "code_id": { + "description": "the code_id of the **new** logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "contract_addr": { "type": "string" }, @@ -3939,12 +4324,6 @@ "$ref": "#/definitions/Binary" } ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -4017,14 +4396,13 @@ } } }, - "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" }, "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); ```", + "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 secret_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" }, "Vote": { @@ -4092,8 +4470,7 @@ } ] } - }, - "additionalProperties": false + } } } }, @@ -4124,7 +4501,6 @@ "$ref": "#/definitions/SingleChoiceProposal" } }, - "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.", @@ -4271,6 +4647,7 @@ ], "properties": { "type_url": { + "description": "this is the fully qualified msg path used for routing, e.g. /cosmos.bank.v1beta1.MsgSend NOTE: the type_url can be changed after a chain upgrade", "type": "string" }, "value": { @@ -4316,6 +4693,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finalize_tx" + ], + "properties": { + "finalize_tx": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false } ] }, @@ -4449,8 +4838,7 @@ ], "properties": { "never": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -4458,7 +4846,6 @@ ] }, "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ { "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", @@ -4480,12 +4867,7 @@ "minimum": 0.0 }, "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] + "$ref": "#/definitions/VoteOption" } } } @@ -4509,6 +4891,7 @@ "required": [ "amount", "channel_id", + "memo", "timeout", "to_address" ], @@ -4522,7 +4905,11 @@ ] }, "channel_id": { - "description": "existing channel to send the tokens over", + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "memo": { + "description": "optional memo can put here `{\"ibc_callback\":\"secret1contractAddr\"}` to get a callback on ack/timeout see this for more info: https://github.com/scrtlabs/SecretNetwork/blob/78a5f82a4/x/ibc-hooks/README.md?plain=1#L144-L188", "type": "string" }, "timeout": { @@ -4640,7 +5027,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -4658,8 +5045,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -4790,8 +5176,7 @@ } ] } - }, - "additionalProperties": false + } }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", @@ -4937,8 +5322,7 @@ "expiration": { "$ref": "#/definitions/Expiration" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -4971,8 +5355,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -4997,8 +5380,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -5019,8 +5401,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -5028,7 +5409,7 @@ ] }, "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); ```", + "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 secret_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" @@ -5036,11 +5417,11 @@ ] }, "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); ```", + "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 secret_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); ```", + "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 secret_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" }, "VetoConfig": { @@ -5072,8 +5453,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } }, "VoteOption": { "type": "string", @@ -5101,8 +5481,7 @@ "yes": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } }, "WasmMsg": { "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", @@ -5117,19 +5496,18 @@ "execute": { "type": "object", "required": [ + "code_hash", "contract_addr", - "funds", - "msg" + "msg", + "send" ], "properties": { - "contract_addr": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", "type": "string" }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "contract_addr": { + "type": "string" }, "msg": { "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", @@ -5138,6 +5516,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -5145,7 +5529,7 @@ "additionalProperties": false }, { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", "type": "object", "required": [ "instantiate" @@ -5154,10 +5538,11 @@ "instantiate": { "type": "object", "required": [ + "code_hash", "code_id", - "funds", "label", - "msg" + "msg", + "send" ], "properties": { "admin": { @@ -5166,19 +5551,17 @@ "null" ] }, + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, "code_id": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, "label": { - "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "description": "A human-readbale label for the contract, must be unique across all contracts", "type": "string" }, "msg": { @@ -5188,6 +5571,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -5204,11 +5593,22 @@ "migrate": { "type": "object", "required": [ + "code_hash", + "code_id", "contract_addr", - "msg", - "new_code_id" + "msg" ], "properties": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the **new** code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, + "code_id": { + "description": "the code_id of the **new** logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "contract_addr": { "type": "string" }, @@ -5219,12 +5619,6 @@ "$ref": "#/definitions/Binary" } ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -5300,8 +5694,7 @@ ], "properties": { "anyone": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -5316,14 +5709,17 @@ "module": { "type": "object", "required": [ - "addr" + "addr", + "code_hash" ], "properties": { "addr": { "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -5347,11 +5743,32 @@ "hooks": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/HookItem" } } }, - "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" + }, + "HookItem": { + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + } + } }, "reverse_proposals": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -5369,7 +5786,6 @@ } } }, - "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.", @@ -5516,6 +5932,7 @@ ], "properties": { "type_url": { + "description": "this is the fully qualified msg path used for routing, e.g. /cosmos.bank.v1beta1.MsgSend NOTE: the type_url can be changed after a chain upgrade", "type": "string" }, "value": { @@ -5561,6 +5978,18 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "finalize_tx" + ], + "properties": { + "finalize_tx": { + "$ref": "#/definitions/Empty" + } + }, + "additionalProperties": false } ] }, @@ -5694,8 +6123,7 @@ ], "properties": { "never": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -5703,7 +6131,6 @@ ] }, "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ { "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", @@ -5725,12 +6152,7 @@ "minimum": 0.0 }, "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] + "$ref": "#/definitions/VoteOption" } } } @@ -5754,6 +6176,7 @@ "required": [ "amount", "channel_id", + "memo", "timeout", "to_address" ], @@ -5767,7 +6190,11 @@ ] }, "channel_id": { - "description": "existing channel to send the tokens over", + "description": "exisiting channel to send the tokens over", + "type": "string" + }, + "memo": { + "description": "optional memo can put here `{\"ibc_callback\":\"secret1contractAddr\"}` to get a callback on ack/timeout see this for more info: https://github.com/scrtlabs/SecretNetwork/blob/78a5f82a4/x/ibc-hooks/README.md?plain=1#L144-L188", "type": "string" }, "timeout": { @@ -5885,7 +6312,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5903,8 +6330,7 @@ ], "properties": { "majority": { - "type": "object", - "additionalProperties": false + "type": "object" } }, "additionalProperties": false @@ -5941,8 +6367,7 @@ "proposal": { "$ref": "#/definitions/SingleChoiceProposal" } - }, - "additionalProperties": false + } }, "SingleChoiceProposal": { "type": "object", @@ -6055,8 +6480,7 @@ } ] } - }, - "additionalProperties": false + } }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", @@ -6202,8 +6626,7 @@ "expiration": { "$ref": "#/definitions/Expiration" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -6236,8 +6659,7 @@ "percentage": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -6262,8 +6684,7 @@ "threshold": { "$ref": "#/definitions/PercentageThreshold" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -6284,8 +6705,7 @@ "threshold": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } } }, "additionalProperties": false @@ -6293,7 +6713,7 @@ ] }, "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); ```", + "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 secret_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" @@ -6301,11 +6721,11 @@ ] }, "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); ```", + "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 secret_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); ```", + "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 secret_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" }, "VetoConfig": { @@ -6337,8 +6757,7 @@ "description": "The address able to veto proposals.", "type": "string" } - }, - "additionalProperties": false + } }, "VoteOption": { "type": "string", @@ -6366,8 +6785,7 @@ "yes": { "$ref": "#/definitions/Uint128" } - }, - "additionalProperties": false + } }, "WasmMsg": { "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", @@ -6382,19 +6800,18 @@ "execute": { "type": "object", "required": [ + "code_hash", "contract_addr", - "funds", - "msg" + "msg", + "send" ], "properties": { - "contract_addr": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", "type": "string" }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } + "contract_addr": { + "type": "string" }, "msg": { "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", @@ -6403,6 +6820,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -6410,7 +6833,7 @@ "additionalProperties": false }, { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", + "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.", "type": "object", "required": [ "instantiate" @@ -6419,10 +6842,11 @@ "instantiate": { "type": "object", "required": [ + "code_hash", "code_id", - "funds", "label", - "msg" + "msg", + "send" ], "properties": { "admin": { @@ -6431,19 +6855,17 @@ "null" ] }, + "code_hash": { + "description": "code_hash is the hex encoded hash of the code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, "code_id": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, "label": { - "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", + "description": "A human-readbale label for the contract, must be unique across all contracts", "type": "string" }, "msg": { @@ -6453,6 +6875,12 @@ "$ref": "#/definitions/Binary" } ] + }, + "send": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } } } } @@ -6469,11 +6897,22 @@ "migrate": { "type": "object", "required": [ + "code_hash", + "code_id", "contract_addr", - "msg", - "new_code_id" + "msg" ], "properties": { + "code_hash": { + "description": "code_hash is the hex encoded hash of the **new** code. This is used by Secret Network to harden against replaying the contract It is used to bind the request to a destination contract in a stronger way than just the contract address which can be faked", + "type": "string" + }, + "code_id": { + "description": "the code_id of the **new** logic to place in the given contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, "contract_addr": { "type": "string" }, @@ -6484,12 +6923,6 @@ "$ref": "#/definitions/Binary" } ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 } } } @@ -6557,11 +6990,32 @@ "hooks": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/HookItem" } } }, - "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" + }, + "HookItem": { + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false + } + } } } } diff --git a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json index 532bdac..9e04ecc 100644 --- a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json +++ b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json @@ -7,11 +7,19 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "group_contract" + "dao_code_hash", + "group_contract", + "query_auth" ], "properties": { + "dao_code_hash": { + "type": "string" + }, "group_contract": { "$ref": "#/definitions/GroupContract" + }, + "query_auth": { + "$ref": "#/definitions/RawContract" } }, "additionalProperties": false, @@ -27,11 +35,15 @@ "existing": { "type": "object", "required": [ - "address" + "address", + "code_hash" ], "properties": { "address": { "type": "string" + }, + "code_hash": { + "type": "string" } }, "additionalProperties": false @@ -48,10 +60,14 @@ "new": { "type": "object", "required": [ + "cw4_group_code_hash", "cw4_group_code_id", "initial_members" ], "properties": { + "cw4_group_code_hash": { + "type": "string" + }, "cw4_group_code_id": { "type": "integer", "format": "uint64", @@ -89,6 +105,23 @@ } }, "additionalProperties": false + }, + "RawContract": { + "description": "A contract that does not contain a validated address. Should be accepted as user input because we shouldn't assume addresses are verified Addrs. https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Addr.html", + "type": "object", + "required": [ + "address", + "code_hash" + ], + "properties": { + "address": { + "type": "string" + }, + "code_hash": { + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -125,11 +158,11 @@ "voting_power_at_height": { "type": "object", "required": [ - "address" + "auth" ], "properties": { - "address": { - "type": "string" + "auth": { + "$ref": "#/definitions/Auth" }, "height": { "type": [ @@ -197,7 +230,161 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "Auth": { + "oneOf": [ + { + "type": "object", + "required": [ + "viewing_key" + ], + "properties": { + "viewing_key": { + "type": "object", + "required": [ + "address", + "key" + ], + "properties": { + "address": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "permit" + ], + "properties": { + "permit": { + "$ref": "#/definitions/Permit_for_PermitData" + } + }, + "additionalProperties": false + } + ] + }, + "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" + }, + "PermitData": { + "type": "object", + "required": [ + "data", + "key" + ], + "properties": { + "data": { + "$ref": "#/definitions/Binary" + }, + "key": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PermitSignature": { + "type": "object", + "required": [ + "pub_key", + "signature" + ], + "properties": { + "pub_key": { + "$ref": "#/definitions/PubKey" + }, + "signature": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + }, + "Permit_for_PermitData": { + "description": "Where the information will be stored", + "type": "object", + "required": [ + "params", + "signature" + ], + "properties": { + "account_number": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "chain_id": { + "type": [ + "string", + "null" + ] + }, + "memo": { + "type": [ + "string", + "null" + ] + }, + "params": { + "$ref": "#/definitions/PermitData" + }, + "sequence": { + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "signature": { + "$ref": "#/definitions/PermitSignature" + } + }, + "additionalProperties": false + }, + "PubKey": { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "description": "ignored, but must be \"tendermint/PubKeySecp256k1\" otherwise the verification will fail", + "type": "string" + }, + "value": { + "description": "Secp256k1 PubKey", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "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 secret_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" + } + } }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -209,15 +396,49 @@ "responses": { "dao": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "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" + "title": "AnyContractInfo", + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "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" + } + } }, "group_contract": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "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" + "title": "AnyContractInfo", + "type": "object", + "required": [ + "addr", + "code_hash" + ], + "properties": { + "addr": { + "$ref": "#/definitions/Addr" + }, + "code_hash": { + "type": "string" + } + }, + "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" + } + } }, "info": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -231,7 +452,6 @@ "$ref": "#/definitions/ContractVersion" } }, - "additionalProperties": false, "definitions": { "ContractVersion": { "type": "object", @@ -271,10 +491,9 @@ "$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); ```", + "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 secret_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" } } @@ -297,10 +516,9 @@ "$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); ```", + "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 secret_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" } } diff --git a/contracts/voting/dao-voting-snip721-roles/Cargo.toml b/contracts/voting/dao-voting-snip721-roles/Cargo.toml index 721bf6d..d6c6d11 100644 --- a/contracts/voting/dao-voting-snip721-roles/Cargo.toml +++ b/contracts/voting/dao-voting-snip721-roles/Cargo.toml @@ -30,7 +30,7 @@ secret-utils = { workspace = true } secret-cw2 = { workspace = true } cw4 = { workspace = true } thiserror = { workspace = true } -snip721-reference-impl ={ workspace=true } +snip721-roles-impl ={ workspace=true } schemars ={ workspace = true } secret-toolkit ={ workspace = true } serde ={ workspace = true } diff --git a/contracts/voting/dao-voting-snip721-roles/src/contract.rs b/contracts/voting/dao-voting-snip721-roles/src/contract.rs index 2da1744..9328fdd 100644 --- a/contracts/voting/dao-voting-snip721-roles/src/contract.rs +++ b/contracts/voting/dao-voting-snip721-roles/src/contract.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use cw4::{MemberResponse, TotalWeightResponse}; use dao_interface::state::AnyContractInfo; -use dao_snip721_extensions::roles::QueryExt; +use dao_snip721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; use secret_cw2::set_contract_version; use secret_utils::parse_reply_event_for_contract_address; use shade_protocol::basic_staking::Auth; @@ -25,7 +25,7 @@ const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result, ContractError> { @@ -54,15 +54,15 @@ pub fn instantiate( NftContract::New { snip721_roles_code_id, snip721_roles_code_hash, - label, name, symbol, - initial_nfts, entropy, config, - snip721_code_id, - snip721_code_hash, query_auth, + admin, + royalty_info, + post_init_callback, + initial_nfts, } => { // Check there is at least one NFT to initialize if initial_nfts.is_empty() { @@ -73,20 +73,20 @@ pub fn instantiate( INITIAL_NFTS.save(deps.storage, &initial_nfts)?; let init_msg = snip721roles::Snip721RolesInstantiateMsg { - code_id: snip721_code_id, - code_hash: snip721_code_hash.clone(), - label: label.clone(), name, symbol, entropy, config, query_auth, + admin, + royalty_info, + post_init_callback, }; // Create instantiate submessage for NFT roles contract let submsg = SubMsg::reply_on_success( init_msg.to_cosmos_msg( Some(info.sender.to_string()), - label.clone(), + env.contract.address.to_string(), snip721_roles_code_id, snip721_roles_code_hash.clone(), None, @@ -137,7 +137,9 @@ pub fn query_voting_power_at_height( let member: MemberResponse = deps.querier.query_wasm_smart( config.nft_code_hash, config.nft_address, - &snip721_roles::msg::QueryMsg::ExtensionQuery(QueryExt::Member { auth, at_height }), + &snip721_roles_impl::msg::QueryMsg::::QueryExtension { + msg: QueryExt::Member { auth, at_height }, + }, )?; to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { @@ -155,7 +157,9 @@ pub fn query_total_power_at_height( let total: TotalWeightResponse = deps.querier.query_wasm_smart( config.nft_code_hash, config.nft_address, - &snip721_roles::msg::QueryMsg::ExtensionQuery(QueryExt::TotalWeight { at_height }), + &snip721_roles_impl::msg::QueryMsg::::QueryExtension { + msg: QueryExt::TotalWeight { at_height }, + }, )?; to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { @@ -198,47 +202,34 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result = initial_nfts + let mint_messages: Vec = initial_nfts .iter() - .flat_map(|nft| -> Result { - Ok(SubMsg::new(WasmMsg::Execute { + .flat_map(|nft| -> Result { + Ok(WasmMsg::Execute { contract_addr: nft_roles_contract_address.clone(), code_hash: config.nft_code_hash.clone(), + msg: to_binary(&snip721_roles_impl::msg::ExecuteMsg::< + MetadataExt, + ExecuteExt, + >::MintNft { + token_id: Some(nft.token_id.clone()), + owner: Some(nft.owner.clone()), + public_metadata: None, + private_metadata: None, + serial_number: None, + royalty_info: None, + transferable: None, + memo: None, + padding: None, + extension: MetadataExt { + role: nft.clone().extension.role, + weight: nft.extension.weight, + }, + })?, funds: vec![], - msg: to_binary(&snip721_roles::msg::ExecuteMsg::Snip721Execute( - Box::new(snip721_roles::snip721::Snip721ExecuteMsg::MintNft { - token_id: Some(nft.token_id.clone()), - owner: Some(nft.owner.clone()), - public_metadata: Some(snip721_roles::snip721::Metadata { - token_uri: Some(nft.token_uri.clone().unwrap()), - extension: Some(snip721_roles::snip721::Extension { - image: None, - image_data: None, - external_url: None, - description: None, - name: None, - attributes: None, - background_color: None, - animation_url: None, - youtube_url: None, - media: None, - protected_attributes: None, - token_subtype: None, - role: Some(nft.extension.role.clone().unwrap()), - weight: nft.extension.weight, - }), - }), - private_metadata: None, - serial_number: None, - royalty_info: None, - transferable: None, - memo: None, - padding: None, - }), - ))?, - })) + }) }) - .collect::>(); + .collect::>(); // Clear space INITIAL_NFTS.remove(deps.storage); @@ -247,12 +238,13 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result::ChangeAdmin { + address: dao.addr.to_string(), + padding: None, + })?, funds: vec![], }; @@ -262,7 +254,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Err(ContractError::NftInstantiateError {}), } diff --git a/contracts/voting/dao-voting-snip721-roles/src/msg.rs b/contracts/voting/dao-voting-snip721-roles/src/msg.rs index 5e45c9f..0882748 100644 --- a/contracts/voting/dao-voting-snip721-roles/src/msg.rs +++ b/contracts/voting/dao-voting-snip721-roles/src/msg.rs @@ -4,6 +4,10 @@ use dao_snip721_extensions::roles::MetadataExt; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use shade_protocol::utils::asset::RawContract; +use snip721_roles_impl::{ + msg::{InstantiateConfig, PostInstantiateCallback}, + royalties::RoyaltyInfo, +}; #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] pub struct NftMintMsg { @@ -19,6 +23,7 @@ pub struct NftMintMsg { pub extension: MetadataExt, } +#[allow(clippy::large_enum_variant)] #[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] pub enum NftContract { Existing { @@ -28,30 +33,31 @@ pub enum NftContract { code_hash: String, }, New { - /// Code ID for snip721 roles token contract. + /// Code ID for snip721 roles contract. snip721_roles_code_id: u64, - /// Code hash for snip721 roles token contract. + /// Code hash for snip721 roles contract. snip721_roles_code_hash: String, - - /// Code ID for snip721 token contract. - snip721_code_id: u64, - /// Code hash for snip721 token contract. - snip721_code_hash: String, - /// Label to use for instantiated snip721 contract. - label: String, - /// NFT collection name - name: String, - /// NFT collection symbol - symbol: String, - /// Initial NFTs to mint when instantiating the new snip721 contract. + /// Initial NFTs to mint when instantiating the new cw721 contract. /// If empty, an error is thrown. initial_nfts: Vec, - + /// name of token contract + name: String, + /// token contract symbol + symbol: String, + /// optional admin address, env.message.sender if missing + admin: Option, /// entropy used for prng seed entropy: String, + /// optional royalty information to use as default when RoyaltyInfo is not provided to a + /// minting function + royalty_info: Option, /// optional privacy configuration for the contract - config: Option, - + config: Option, + /// optional callback message to execute after instantiation. This will + /// most often be used to have the token contract provide its address to a + /// contract that instantiated it, but it could be used to execute any + /// contract + post_init_callback: Option, query_auth: RawContract, }, } diff --git a/contracts/voting/dao-voting-snip721-roles/src/snip721roles.rs b/contracts/voting/dao-voting-snip721-roles/src/snip721roles.rs index 7a17d32..e13d82a 100644 --- a/contracts/voting/dao-voting-snip721-roles/src/snip721roles.rs +++ b/contracts/voting/dao-voting-snip721-roles/src/snip721roles.rs @@ -1,83 +1,458 @@ +use cosmwasm_std::Binary; +use dao_snip721_extensions::roles::{ExecuteExt, MetadataExt}; use schemars::JsonSchema; -use secret_toolkit::utils::InitCallback; +use secret_toolkit::utils::{HandleCallback, InitCallback}; use serde::{Deserialize, Serialize}; use shade_protocol::utils::asset::RawContract; +use snip721_roles_impl::{ + expiration::Expiration, + msg::{ + AccessLevel, Burn, ContractStatus, InstantiateConfig, Mint, PostInstantiateCallback, + ReceiverInfo, Send, Transfer, + }, + royalties::RoyaltyInfo, +}; #[derive(Serialize, Deserialize, JsonSchema)] pub struct Snip721RolesInstantiateMsg { - /// Code ID for snip721 token contract. - pub code_id: u64, - /// Code hash for snip721 token contract. - pub code_hash: String, - /// Label to use for instantiated snip721 contract. - pub label: String, - /// NFT collection name + /// name of token contract pub name: String, - /// NFT collection symbol + /// token contract symbol pub symbol: String, - + /// optional admin address, env.message.sender if missing + pub admin: Option, /// entropy used for prng seed pub entropy: String, - + /// optional royalty information to use as default when RoyaltyInfo is not provided to a + /// minting function + pub royalty_info: Option, /// optional privacy configuration for the contract pub config: Option, - + /// optional callback message to execute after instantiation. This will + /// most often be used to have the token contract provide its address to a + /// contract that instantiated it, but it could be used to execute any + /// contract + pub post_init_callback: Option, pub query_auth: RawContract, } -/// This type represents optional configuration values. -/// All values are optional and have defaults which are more private by default, -/// but can be overridden if necessary -#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)] -pub struct InstantiateConfig { - /// indicates whether the token IDs and the number of tokens controlled by the contract are - /// public. If the token supply is private, only minters can view the token IDs and - /// number of tokens controlled by the contract - /// default: False - pub public_token_supply: Option, - /// indicates whether token ownership is public or private. A user can still change whether the - /// ownership of their tokens is public or private - /// default: False - pub public_owner: Option, - /// indicates whether sealed metadata should be enabled. If sealed metadata is enabled, the - /// private metadata is not viewable by anyone, not even the owner, until the owner calls the - /// Reveal function. When Reveal is called, the sealed metadata is irreversibly moved to the - /// public metadata (as default). if unwrapped_metadata_is_private is set to true, it will - /// remain as private metadata, but the owner will now be able to see it. Anyone will be able - /// to query the token to know that it has been unwrapped. This simulates buying/selling a - /// wrapped card that no one knows which card it is until it is unwrapped. If sealed metadata - /// is not enabled, all tokens are considered unwrapped - /// default: False - pub enable_sealed_metadata: Option, - /// indicates if the Reveal function should keep the sealed metadata private after unwrapping - /// This config value is ignored if sealed metadata is not enabled - /// default: False - pub unwrapped_metadata_is_private: Option, - /// indicates whether a minter is permitted to update a token's metadata - /// default: True - pub minter_may_update_metadata: Option, - /// indicates whether the owner of a token is permitted to update a token's metadata - /// default: False - pub owner_may_update_metadata: Option, - /// Indicates whether burn functionality should be enabled - /// default: False - pub enable_burn: Option, +impl InitCallback for Snip721RolesInstantiateMsg { + const BLOCK_SIZE: usize = 264; } -impl Default for InstantiateConfig { - fn default() -> Self { - InstantiateConfig { - public_token_supply: Some(false), - public_owner: Some(false), - enable_sealed_metadata: Some(false), - unwrapped_metadata_is_private: Some(false), - minter_may_update_metadata: Some(true), - owner_may_update_metadata: Some(false), - enable_burn: Some(false), - } - } +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Snip721RolesExecuteMsg { + /// mint new token + MintNft { + /// optional token id. if omitted, use current token index + token_id: Option, + /// optional owner address. if omitted, owned by the message sender + owner: Option, + /// optional public metadata that can be seen by everyone + public_metadata: Option, + /// optional private metadata that can only be seen by the owner and whitelist + private_metadata: Option, + /// optional serial number for this token + serial_number: Option, + /// optional royalty information for this token. This will be ignored if the token is + /// non-transferable + royalty_info: Option, + /// optionally true if the token is transferable. Defaults to true if omitted + transferable: Option, + /// optional memo for the tx + memo: Option, + /// optional message length padding + padding: Option, + /// Any custom extension used by this contract + extension: MetadataExt, + }, + /// Mint multiple tokens + BatchMintNft { + /// list of mint operations to perform + mints: Vec>, + /// optional message length padding + padding: Option, + }, + /// create a mint run of clones that will have MintRunInfos showing they are serialized + /// copies in the same mint run with the specified quantity. Mint_run_id can be used to + /// track mint run numbers in subsequent MintNftClones calls. So, if provided, the first + /// MintNftClones call will have mint run number 1, the next time MintNftClones is called + /// with the same mint_run_id, those clones will have mint run number 2, etc... If no + /// mint_run_id is specified, the clones will not have any mint run number assigned to their + /// MintRunInfos. Because this mints to a single address, there is no option to specify + /// that the clones are non-transferable as there is no foreseen reason for someone to have + /// multiple copies of an nft that they can never send to others + MintNftClones { + /// optional mint run ID + mint_run_id: Option, + /// number of clones to mint + quantity: u32, + /// optional owner address. if omitted, owned by the message sender + owner: Option, + /// optional public metadata that can be seen by everyone + public_metadata: Option, + /// optional private metadata that can only be seen by the owner and whitelist + private_metadata: Option, + /// optional royalty information for these tokens + royalty_info: Option, + /// optional memo for the mint txs + memo: Option, + /// optional message length padding + padding: Option, + /// Any custom extension used by this contract + extension: MetadataExt, + }, + /// set the public and/or private metadata. This can be called by either the token owner or + /// a valid minter if they have been given this power by the appropriate config values + SetMetadata { + /// id of the token whose metadata should be updated + token_id: String, + /// the optional new public metadata + public_metadata: Option, + /// the optional new private metadata + private_metadata: Option, + /// optional message length padding + padding: Option, + }, + /// set royalty information. If no token ID is provided, this royalty info will become the default + /// RoyaltyInfo for any new tokens minted on the contract. If a token ID is provided, this can only + /// be called by the token creator and only when the creator is the current owner. Royalties can not + /// be set on a token that is not transferable, because they can never be sold + SetRoyaltyInfo { + /// optional id of the token whose royalty information should be updated. If not provided, + /// this updates the default royalty information for any new tokens minted on the contract + token_id: Option, + /// the new royalty information. If None, existing royalty information will be deleted. It should + /// be noted, that if deleting a token's royalty information while the contract has a default royalty + /// info set up will give the token the default royalty information + royalty_info: Option, + /// optional message length padding + padding: Option, + }, + /// Reveal the private metadata of a sealed token and mark the token as having been unwrapped + Reveal { + /// id of the token to unwrap + token_id: String, + /// optional message length padding + padding: Option, + }, + /// if a contract was instantiated to make ownership public by default, this will allow + /// an address to make the ownership of their tokens private. The address can still use + /// SetGlobalApproval to make ownership public either inventory-wide or for a specific token + MakeOwnershipPrivate { + /// optional message length padding + padding: Option, + }, + /// add/remove approval(s) that whitelist everyone (makes public) + SetGlobalApproval { + /// optional token id to apply approval/revocation to + token_id: Option, + /// optional permission level for viewing the owner + view_owner: Option, + /// optional permission level for viewing private metadata + view_private_metadata: Option, + /// optional expiration + expires: Option, + /// optional message length padding + padding: Option, + }, + /// add/remove approval(s) for a specific address on the token(s) you own. Any permissions + /// that are omitted will keep the current permission setting for that whitelist address + SetWhitelistedApproval { + /// address being granted/revoked permission + address: String, + /// optional token id to apply approval/revocation to + token_id: Option, + /// optional permission level for viewing the owner + view_owner: Option, + /// optional permission level for viewing private metadata + view_private_metadata: Option, + /// optional permission level for transferring + transfer: Option, + /// optional expiration + expires: Option, + /// optional message length padding + padding: Option, + }, + /// gives the spender permission to transfer the specified token. If you are the owner + /// of the token, you can use SetWhitelistedApproval to accomplish the same thing. If + /// you are an operator, you can only use Approve + Approve { + /// address being granted the permission + spender: String, + /// id of the token that the spender can transfer + token_id: String, + /// optional expiration for this approval + expires: Option, + /// optional message length padding + padding: Option, + }, + /// revokes the spender's permission to transfer the specified token. If you are the owner + /// of the token, you can use SetWhitelistedApproval to accomplish the same thing. If you + /// are an operator, you can only use Revoke, but you can not revoke the transfer approval + /// of another operator + Revoke { + /// address whose permission is revoked + spender: String, + /// id of the token that the spender can no longer transfer + token_id: String, + /// optional message length padding + padding: Option, + }, + /// provided for cw721 compliance, but can be done with SetWhitelistedApproval... + /// gives the operator permission to transfer all of the message sender's tokens + ApproveAll { + /// address being granted permission to transfer + operator: String, + /// optional expiration for this approval + expires: Option, + /// optional message length padding + padding: Option, + }, + /// provided for cw721 compliance, but can be done with SetWhitelistedApproval... + /// revokes the operator's permission to transfer any of the message sender's tokens + RevokeAll { + /// address whose permissions are revoked + operator: String, + /// optional message length padding + padding: Option, + }, + /// transfer a token if it is transferable + TransferNft { + /// recipient of the transfer + recipient: String, + /// id of the token to transfer + token_id: String, + /// optional memo for the tx + memo: Option, + /// optional message length padding + padding: Option, + }, + /// transfer many tokens and fails if any are non-transferable + BatchTransferNft { + /// list of transfers to perform + transfers: Vec, + /// optional message length padding + padding: Option, + }, + /// send a token if it is transferable and call the receiving contract's (Batch)ReceiveNft + SendNft { + /// address to send the token to + contract: String, + /// optional code hash and BatchReceiveNft implementation status of the recipient contract + receiver_info: Option, + /// id of the token to send + token_id: String, + /// optional message to send with the (Batch)RecieveNft callback + msg: Option, + /// optional memo for the tx + memo: Option, + /// optional message length padding + padding: Option, + }, + /// send many tokens and call the receiving contracts' (Batch)ReceiveNft. Fails if any tokens are + /// non-transferable + BatchSendNft { + /// list of sends to perform + sends: Vec, + /// optional message length padding + padding: Option, + }, + /// burn a token. This can be always be done on a non-transferable token, regardless of whether burn + /// has been enabled on the contract. An owner should always have a way to get rid of a token they do + /// not want, and burning is the only way to do that if the token is non-transferable + BurnNft { + /// token to burn + token_id: String, + /// optional memo for the tx + memo: Option, + /// optional message length padding + padding: Option, + }, + /// burn many tokens. This can be always be done on a non-transferable token, regardless of whether burn + /// has been enabled on the contract. An owner should always have a way to get rid of a token they do + /// not want, and burning is the only way to do that if the token is non-transferable + BatchBurnNft { + /// list of burns to perform + burns: Vec, + /// optional message length padding + padding: Option, + }, + /// register that the message sending contract implements ReceiveNft and possibly + /// BatchReceiveNft. If a contract implements BatchReceiveNft, SendNft will always + /// call BatchReceiveNft even if there is only one token transferred (the token_ids + /// Vec will only contain one ID) + RegisterReceiveNft { + /// receving contract's code hash + code_hash: String, + /// optionally true if the contract also implements BatchReceiveNft. Defaults + /// to false if not specified + also_implements_batch_receive_nft: Option, + /// optional message length padding + padding: Option, + }, + /// create a viewing key + CreateViewingKey { + /// entropy String used in random key generation + entropy: String, + /// optional message length padding + padding: Option, + }, + /// set viewing key + SetViewingKey { + /// desired viewing key + key: String, + /// optional message length padding + padding: Option, + }, + /// add addresses with minting authority + AddMinters { + /// list of addresses that can now mint + minters: Vec, + /// optional message length padding + padding: Option, + }, + /// revoke minting authority from addresses + RemoveMinters { + /// list of addresses no longer allowed to mint + minters: Vec, + /// optional message length padding + padding: Option, + }, + /// define list of addresses with minting authority + SetMinters { + /// list of addresses with minting authority + minters: Vec, + /// optional message length padding + padding: Option, + }, + /// change address with administrative power + ChangeAdmin { + /// address with admin authority + address: String, + /// optional message length padding + padding: Option, + }, + /// set contract status level to determine which functions are allowed. StopTransactions + /// status prevent mints, burns, sends, and transfers, but allows all other functions + SetContractStatus { + /// status level + level: ContractStatus, + /// optional message length padding + padding: Option, + }, + /// disallow the use of a permit + RevokePermit { + /// name of the permit that is no longer valid + permit_name: String, + /// optional message length padding + padding: Option, + }, + /// Extension msg + Extension { msg: ExecuteExt }, } -impl InitCallback for Snip721RolesInstantiateMsg { +/// Serial number to give an NFT when minting +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] +pub struct SerialNumber { + /// optional number of the mint run this token will be minted in. A mint run represents a + /// batch of NFTs released at the same time. So if a creator decided to make 100 copies + /// of an NFT, they would all be part of mint run number 1. If they sold quickly, and + /// the creator wanted to rerelease that NFT, he could make 100 more copies which would all + /// be part of mint run number 2. + pub mint_run: Option, + /// serial number (in this mint run). This is used to serialize + /// identical NFTs + pub serial_number: u32, + /// optional total number of NFTs minted on this run. This is used to + /// represent that this token is number m of n + pub quantity_minted_this_run: Option, +} + +/// token metadata +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Metadata { + /// optional uri for off-chain metadata. This should be prefixed with `http://`, `https://`, `ipfs://`, or + /// `ar://`. Only use this if you are not using `extension` + pub token_uri: Option, + /// optional on-chain metadata. Only use this if you are not using `token_uri` + pub extension: Option, +} + +/// metadata extension +/// You can add any metadata fields you need here. These fields are based on +/// https://docs.opensea.io/docs/metadata-standards and are the metadata fields that +/// Stashh uses for robust NFT display. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or +/// `ar://` +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Extension { + /// url to the image + pub image: Option, + /// raw SVG image data (not recommended). Only use this if you're not including the image parameter + pub image_data: Option, + /// url to allow users to view the item on your site + pub external_url: Option, + /// item description + pub description: Option, + /// name of the item + pub name: Option, + /// item attributes + pub attributes: Option>, + /// background color represented as a six-character hexadecimal without a pre-pended # + pub background_color: Option, + /// url to a multimedia attachment + pub animation_url: Option, + /// url to a YouTube video + pub youtube_url: Option, + /// media files as specified on Stashh that allows for basic authenticatiion and decryption keys. + /// Most of the above is used for bridging public eth NFT metadata easily, whereas `media` will be used + /// when minting NFTs on Stashh + pub media: Option>, + /// a select list of trait_types that are in the private metadata. This will only ever be used + /// in public metadata + pub protected_attributes: Option>, + /// token subtypes used by Stashh for display groupings (primarily used for badges, which are specified + /// by using "badge" as the token_subtype) + pub token_subtype: Option, +} + +/// attribute trait +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Trait { + /// indicates how a trait should be displayed + pub display_type: Option, + /// name of the trait + pub trait_type: Option, + /// trait value + pub value: String, + /// optional max value for numerical traits + pub max_value: Option, +} + +/// media file +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct MediaFile { + /// file type + /// Stashh currently uses: "image", "video", "audio", "text", "font", "application" + pub file_type: Option, + /// file extension + pub extension: Option, + /// authentication information + pub authentication: Option, + /// url to the file. Urls should be prefixed with `http://`, `https://`, `ipfs://`, or `ar://` + pub url: String, +} + +/// media file authentication +#[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug, Default)] +pub struct Authentication { + /// either a decryption key for encrypted files or a password for basic authentication + pub key: Option, + /// username used in basic authentication + pub user: Option, +} + +impl HandleCallback for Snip721RolesExecuteMsg { const BLOCK_SIZE: usize = 264; } diff --git a/packages/dao-pre-propose-base/Cargo.toml b/packages/dao-pre-propose-base/Cargo.toml index 4074fff..26ad1cd 100644 --- a/packages/dao-pre-propose-base/Cargo.toml +++ b/packages/dao-pre-propose-base/Cargo.toml @@ -31,6 +31,8 @@ serde = { workspace = true } schemars = { workspace = true } thiserror = { workspace = true } shade-protocol ={ workspace = true } +secret-multi-test = { workspace = true } +query_auth ={ workspace = true } [dev-dependencies] secret-multi-test = { workspace = true } diff --git a/packages/dao-pre-propose-base/src/lib.rs b/packages/dao-pre-propose-base/src/lib.rs index 6ca223e..be00400 100644 --- a/packages/dao-pre-propose-base/src/lib.rs +++ b/packages/dao-pre-propose-base/src/lib.rs @@ -5,5 +5,5 @@ pub mod execute; pub mod msg; pub mod state; -// #[cfg(test)] -// mod tests; +#[cfg(test)] +mod tests; diff --git a/packages/dao-pre-propose-base/src/tests.rs b/packages/dao-pre-propose-base/src/tests.rs index 1bc68e7..537d23c 100644 --- a/packages/dao-pre-propose-base/src/tests.rs +++ b/packages/dao-pre-propose-base/src/tests.rs @@ -1,10 +1,13 @@ -use cw_hooks::HooksResponse; -use dao_voting::status::Status; use cosmwasm_std::{ from_binary, testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg, + to_binary, Addr, Binary, ContractInfo, ContractResult, Empty, MessageInfo, Response, SubMsg, + WasmMsg, }; +use cw_hooks::{HookItem, HooksResponse}; +use dao_interface::state::AnyContractInfo; +use dao_voting::status::Status; +use secret_multi_test::{App, Contract as SecretContract, ContractWrapper, Executor}; use crate::{ error::PreProposeError, @@ -14,6 +17,56 @@ use crate::{ type Contract = PreProposeContract; +pub fn query_auth_contract() -> Box> { + let contract = ContractWrapper::new( + query_auth::contract::execute, + query_auth::contract::instantiate, + query_auth::contract::query, + ); + Box::new(contract) +} + +pub fn instantiate_query_auth(app: &mut App) -> ContractInfo { + let query_auth_info = app.store_code(query_auth_contract()); + let msg = shade_protocol::contract_interfaces::query_auth::InstantiateMsg { + admin_auth: shade_protocol::Contract { + address: Addr::unchecked("admin_contract"), + code_hash: "code_hash".to_string(), + }, + prng_seed: to_binary("seed").unwrap(), + }; + + app.instantiate_contract( + query_auth_info, + Addr::unchecked("CREATOR_ADDR"), + &msg, + &[], + "query_auth", + None, + ) + .unwrap() +} + +pub fn create_viewing_key(app: &mut App, contract_info: ContractInfo, info: MessageInfo) -> String { + let msg = shade_protocol::contract_interfaces::query_auth::ExecuteMsg::CreateViewingKey { + entropy: "entropy".to_string(), + padding: None, + }; + let res = app + .execute_contract(info.sender, &contract_info, &msg, &[]) + .unwrap(); + let mut viewing_key = String::new(); + let data: shade_protocol::contract_interfaces::query_auth::ExecuteAnswer = + from_binary(&res.data.unwrap()).unwrap(); + if let shade_protocol::contract_interfaces::query_auth::ExecuteAnswer::CreateViewingKey { + key, + } = data + { + viewing_key = key; + }; + viewing_key +} + #[test] fn test_completed_hook_status_invariant() { let mut deps = mock_dependencies(); @@ -23,7 +76,13 @@ fn test_completed_hook_status_invariant() { module .proposal_module - .save(&mut deps.storage, &Addr::unchecked("pm")) + .save( + &mut deps.storage, + &AnyContractInfo { + addr: Addr::unchecked("pm"), + code_hash: "pm_code_hash".to_string(), + }, + ) .unwrap(); let res = module.execute( @@ -52,7 +111,13 @@ fn test_completed_hook_auth() { module .proposal_module - .save(&mut deps.storage, &Addr::unchecked("pm")) + .save( + &mut deps.storage, + &AnyContractInfo { + addr: Addr::unchecked("pm"), + code_hash: "pm_code_hash".to_string(), + }, + ) .unwrap(); let res = module.execute( @@ -70,16 +135,29 @@ fn test_completed_hook_auth() { #[test] fn test_proposal_submitted_hooks() { + let mut app = App::default(); let mut deps = mock_dependencies(); let module = Contract::default(); module .dao - .save(&mut deps.storage, &Addr::unchecked("d")) + .save( + &mut deps.storage, + &AnyContractInfo { + addr: Addr::unchecked("d"), + code_hash: "d_code_hash".to_string(), + }, + ) .unwrap(); module .proposal_module - .save(&mut deps.storage, &Addr::unchecked("pm")) + .save( + &mut deps.storage, + &AnyContractInfo { + addr: Addr::unchecked("pm"), + code_hash: "pm_code_hash".to_string(), + }, + ) .unwrap(); module .config @@ -95,7 +173,12 @@ fn test_proposal_submitted_hooks() { // The DAO can add a hook. let info = mock_info("d", &[]); module - .execute_add_proposal_submitted_hook(deps.as_mut(), info, "one".to_string()) + .execute_add_proposal_submitted_hook( + deps.as_mut(), + info, + "one".to_string(), + "one_code_hash".to_string(), + ) .unwrap(); let hooks: HooksResponse = from_binary( &module @@ -107,12 +190,23 @@ fn test_proposal_submitted_hooks() { .unwrap(), ) .unwrap(); - assert_eq!(hooks.hooks, vec!["one".to_string()]); + assert_eq!( + hooks.hooks, + vec![HookItem { + addr: Addr::unchecked("one"), + code_hash: "one_code_hash".to_string(), + }] + ); // Non-DAO addresses can not add hooks. let info = mock_info("n", &[]); let err = module - .execute_add_proposal_submitted_hook(deps.as_mut(), info, "two".to_string()) + .execute_add_proposal_submitted_hook( + deps.as_mut(), + info, + "two".to_string(), + "two_code_hash".to_string(), + ) .unwrap_err(); assert_eq!(err, PreProposeError::NotDao {}); @@ -120,6 +214,8 @@ fn test_proposal_submitted_hooks() { // for responding to the next proposal ID query that gets fired by propose. cosmwasm_std::SystemResult::Ok(ContractResult::Ok(to_binary(&1u64).unwrap())) }); + let query_auth = instantiate_query_auth(&mut app); + let viewing_key = create_viewing_key(&mut app, query_auth, mock_info("a", &[])); // The hooks fire when a proposal is created. let res = module @@ -128,6 +224,10 @@ fn test_proposal_submitted_hooks() { mock_env(), mock_info("a", &[]), ExecuteMsg::Propose { + auth: shade_protocol::basic_staking::Auth::ViewingKey { + key: viewing_key, + address: "a".to_string(), + }, msg: Empty::default(), }, ) @@ -136,7 +236,7 @@ fn test_proposal_submitted_hooks() { res.messages[1], SubMsg::new(WasmMsg::Execute { contract_addr: "one".to_string(), - code_hash: mock_env().contract.code_hash, + code_hash: "one_code_hash".to_string(), msg: to_binary(&Empty::default()).unwrap(), funds: vec![], }) @@ -145,14 +245,24 @@ fn test_proposal_submitted_hooks() { // Non-DAO addresses can not remove hooks. let info = mock_info("n", &[]); let err = module - .execute_remove_proposal_submitted_hook(deps.as_mut(), info, "one".to_string()) + .execute_remove_proposal_submitted_hook( + deps.as_mut(), + info, + "one".to_string(), + "one_code_hash".to_string(), + ) .unwrap_err(); assert_eq!(err, PreProposeError::NotDao {}); // The DAO can remove a hook. let info = mock_info("d", &[]); module - .execute_remove_proposal_submitted_hook(deps.as_mut(), info, "one".to_string()) + .execute_remove_proposal_submitted_hook( + deps.as_mut(), + info, + "one".to_string(), + "one_code_hash".to_string(), + ) .unwrap(); let hooks: HooksResponse = from_binary( &module