├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-AGPL.md ├── LICENSE-APACHE.md ├── README.md ├── contracts ├── cosmons │ ├── .cargo │ │ └── config │ ├── Cargo.lock │ ├── Cargo.toml │ ├── NOTICE │ ├── README.md │ ├── examples │ │ └── schema.rs │ ├── helpers.ts │ ├── schema │ │ ├── all_nft_info_response.json │ │ ├── approved_for_all_response.json │ │ ├── contract_info_response.json │ │ ├── handle_msg.json │ │ ├── init_msg.json │ │ ├── minter_response.json │ │ ├── nft_info_response.json │ │ ├── num_tokens_response.json │ │ ├── owner_of_response.json │ │ ├── query_msg.json │ │ └── tokens_response.json │ ├── script.json │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ ├── package.rs │ │ └── state.rs └── marketplace │ ├── .cargo │ └── config │ ├── Cargo.toml │ ├── HOWTO.md │ ├── README.md │ ├── examples │ └── schema.rs │ ├── schema │ ├── buy_nft.json │ ├── contract_info_response.json │ ├── handle_msg.json │ ├── init_msg.json │ ├── offerings_response.json │ ├── query_msg.json │ ├── query_offerings_result.json │ └── sell_nft.json │ └── src │ ├── contract.rs │ ├── error.rs │ ├── lib.rs │ ├── msg.rs │ ├── package.rs │ └── state.rs └── packages ├── cw0 ├── Cargo.toml ├── NOTICE ├── README.md └── src │ ├── balance.rs │ ├── expiration.rs │ ├── lib.rs │ └── pagination.rs ├── cw1 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md ├── examples │ └── schema.rs ├── schema │ ├── can_send_response.json │ ├── handle_msg.json │ └── query_msg.json └── src │ ├── helpers.rs │ ├── lib.rs │ ├── msg.rs │ └── query.rs ├── cw2 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md └── src │ └── lib.rs ├── cw20 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md ├── examples │ └── schema.rs ├── schema │ ├── all_accounts_response.json │ ├── all_allowances_response.json │ ├── allowance_response.json │ ├── balance_response.json │ ├── cw20_handle_msg.json │ ├── cw20_query_msg.json │ ├── cw20_receive_msg.json │ ├── minter_response.json │ └── token_info_response.json └── src │ ├── balance.rs │ ├── coin.rs │ ├── helpers.rs │ ├── lib.rs │ ├── msg.rs │ ├── query.rs │ └── receiver.rs ├── cw3 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md ├── examples │ └── schema.rs ├── schema │ ├── handle_msg.json │ ├── proposal_list_response.json │ ├── proposal_response.json │ ├── query_msg.json │ ├── threshold_response.json │ ├── vote_list_response.json │ ├── vote_response.json │ ├── voter_list_response.json │ └── voter_response.json └── src │ ├── helpers.rs │ ├── lib.rs │ ├── msg.rs │ └── query.rs ├── cw4 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md ├── examples │ └── schema.rs ├── schema │ ├── admin_response.json │ ├── cw4_handle_msg.json │ ├── cw4_init_msg.json │ ├── cw4_query_msg.json │ ├── member_list_response.json │ ├── member_response.json │ └── total_weight_response.json └── src │ ├── helpers.rs │ ├── lib.rs │ ├── msg.rs │ └── query.rs ├── cw721 ├── .cargo │ └── config ├── Cargo.toml ├── NOTICE ├── README.md ├── examples │ └── schema.rs ├── schema │ ├── all_nft_info_response.json │ ├── approved_for_all_response.json │ ├── contract_info_response.json │ ├── cw721_handle_msg.json │ ├── cw721_query_msg.json │ ├── cw721_receive_msg.json │ ├── nft_info_response.json │ ├── num_tokens_response.json │ ├── owner_of_response.json │ └── tokens_response.json └── src │ ├── helpers.rs │ ├── lib.rs │ ├── msg.rs │ ├── query.rs │ └── receiver.rs └── storage-plus ├── Cargo.toml ├── NOTICE ├── README.md └── src ├── endian.rs ├── helpers.rs ├── indexed_map.rs ├── indexes.rs ├── item.rs ├── iter_helpers.rs ├── keys.rs ├── legacy_helpers.rs ├── lib.rs ├── map.rs ├── path.rs └── prefix.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Text file backups 5 | **/*.rs.bk 6 | 7 | # Build results 8 | target/ 9 | 10 | # IDEs 11 | .vscode/ 12 | .idea/ 13 | *.iml 14 | 15 | # Auto-gen 16 | .cargo-ok 17 | 18 | # Build artifacts 19 | *.wasm 20 | hash.txt 21 | contracts.txt 22 | artifacts/ 23 | 24 | # Keys should be uploaded 25 | *.key 26 | 27 | # Internal stuff 28 | TRANSCRIPT.md -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["packages/*", "contracts/*"] 3 | 4 | [profile.release.package.cosmons] 5 | opt-level = 3 6 | debug = false 7 | debug-assertions = false 8 | codegen-units = 1 9 | incremental = false 10 | 11 | [profile.release] 12 | rpath = false 13 | lto = true 14 | overflow-checks = true 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmons - An NFT Example for Managing Digital Collectibles 2 | 3 | ## How to Build 4 | 5 | In order to optimize your smart contracts, you have to use: 6 | 7 | ```shell 8 | docker run --rm -v "$(pwd)":/code \ 9 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ 10 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 11 | cosmwasm/workspace-optimizer:0.10.4 12 | ``` 13 | 14 | ## How to Work with REPL 15 | 16 | `npx @cosmjs/cli@^0.23 --init contracts/cosmons/helpers.ts` 17 | 18 | :warning: Please, keep in mind that helper.ts uses a local wasmd 0.11.1 instance (localnetOptions). Please, update your parameters accordingly. 19 | 20 | For hackatom net, you need to use `defaultOptions` in useOptions. 21 | 22 | ### Using a contract 23 | 24 | ```typescript 25 | // Hackatom_V Net 26 | const client = await useOptions(defaultOptions).setup(); 27 | const partner = await useOptions(defaultOptions).setup(, "/Users/user/.hackatom2.key"); 28 | 29 | // localnet 30 | const client = await useOptions(localnetOptions).setup(, "/Users/user/localnet.key"); 31 | const partner = await useOptions(localnetOptions).setup(, "/Users/user/localnet2.key"); 32 | 33 | const address = client.senderAddress; 34 | const partnerAddr = partner.senderAddress; 35 | ``` 36 | 37 | ### Get the Factory 38 | 39 | ```typescript 40 | const cw721 = CW721(client); 41 | ``` 42 | 43 | ### Verify Funds in the Account 44 | 45 | ```typescript 46 | client.getAccount() 47 | partner.getAccount() 48 | ``` 49 | 50 | ### Use Existing Accounts 51 | 52 | You can skip this section if followed this transcript up until here. 53 | 54 | ```typescript 55 | const fred = "cosmos1rgd5jtgp22vq44xz4c69x5z9mu0q92ujcnqdgw"; 56 | const bob = "cosmos1exmd9ml0adgkuggd6knqcjgw4e3x84r4hhfr07"; 57 | ``` 58 | 59 | Query accounts: 60 | `wasmcli query account $(wasmcli keys show -a fred)` 61 | `wasmcli query account $(wasmcli keys show -a vhx)` 62 | 63 | ### Instantiate the Contract 64 | 65 | ```typescript 66 | const codeId = ; // wasmcli q wasm list-code & find your contract ID 67 | const initMsg = { name: "Cosmons", symbol: "mons", minter: address }; 68 | const contract = await client.instantiate(codeId, initMsg, "Virtual Cosmons 1"); 69 | ``` 70 | 71 | **OR** 72 | 73 | ```typescript 74 | const contract = client.getContracts(); // And check for your contractAddress 75 | ``` 76 | 77 | ### Use Contract 78 | 79 | ```typescript 80 | const mine = cw721.use(contract.contractAddress); 81 | ``` 82 | 83 | ### Mint a Cosmon NFT 84 | 85 | ```typescript 86 | mine.mint("monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb", address, "Cosmos", "Minted Cosmon!"); 87 | ``` 88 | 89 | ### Approve Token Transfer 90 | 91 | > :warning: Needs to be called before `transferNft`. 92 | 93 | ```typescript 94 | mine.approve(address, "monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb"); 95 | ``` 96 | 97 | ### Revoke Token Transfer 98 | 99 | > :warning: `transferNft` will not work after using `revoke`. 100 | 101 | ```typescript 102 | mine.revoke(address, "monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb"); 103 | ``` 104 | 105 | ### Transfer Token to Partner 106 | 107 | > :warning: Needs to be called after `approve`. 108 | 109 | ```typescript 110 | mine.transferNft(partnerAddr, "monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb"); 111 | ``` 112 | 113 | #### Queries 114 | 115 | ```typescript 116 | mine.nftInfo("monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb") 117 | mine.ownerOf("monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb") 118 | mine.numTokens() 119 | mine.tokens(address, "", 10) 120 | mine.allNftInfo("monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb") 121 | mine.allTokens("", 10) 122 | ``` 123 | 124 | ### Errata 125 | 126 | Faucet is not supported. 127 | 128 | ## Licenses 129 | 130 | This repo contains two license, [Apache 2.0](./LICENSE-APACHE) and 131 | [AGPL 3.0](./LICENSE-AGPL.md). All crates in this repo may be licensed 132 | as one or the other. Please check the `NOTICE` in each crate or the 133 | relevant `Cargo.toml` file for clarity. 134 | 135 | All *specifications* will always be Apache-2.0. All contracts that are 136 | meant to be *building blocks* will also be Apache-2.0. This is along 137 | the lines of Open Zepellin or other public references. 138 | 139 | Contracts that are "ready to deploy" may be licensed under AGPL 3.0 to 140 | encourage anyone using them to contribute back any improvements they 141 | make. This is common practice for actual projects running on Ethereum, 142 | like Uniswap or Maker DAO. 143 | 144 | -------------------------------------------------------------------------------- /contracts/cosmons/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib --features backtraces" 5 | schema = "run --example schema" 6 | -------------------------------------------------------------------------------- /contracts/cosmons/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmons" 3 | version = "0.0.0" 4 | authors = ["blockscape-dev@mwaysolutions.com"] 5 | edition = "2018" 6 | description = "Basic implementation cw721 NFTs" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | exclude = [ 13 | # 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. 14 | "artifacts/*", 15 | ] 16 | 17 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 18 | [lib] 19 | crate-type = ["cdylib", "rlib"] 20 | 21 | [features] 22 | # for more explicit tests, cargo test --features=backtraces 23 | backtraces = ["cosmwasm-std/backtraces"] 24 | # use library feature to disable all init/handle/query exports 25 | library = [] 26 | 27 | [dependencies] 28 | cw-storage-plus = { version = "0.3.2", features = ["iterator"] } 29 | cw0 = { version = "0.3.2" } 30 | cw2 = { version = "0.3.2" } 31 | cw721 = { version = "0.3.2" } 32 | 33 | cosmwasm-std = { version = "0.11.1" } 34 | schemars = "0.7" 35 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 36 | thiserror = { version = "1.0.20" } 37 | 38 | [dev-dependencies] 39 | cosmwasm-schema = { version = "0.11.1" } 40 | -------------------------------------------------------------------------------- /contracts/cosmons/NOTICE: -------------------------------------------------------------------------------- 1 | Cosmos 2 | Copyright (C) 2020 M-Way Solutions GmbH 3 | Copyright (C) 2020 Confio OÜ 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /contracts/cosmons/README.md: -------------------------------------------------------------------------------- 1 | # Cw721 Basic 2 | 3 | This is a basic implementation of a cw721 NFT contract. It implements 4 | the [CW721 spec](../../packages/cw721/README.md) and is designed to 5 | be deployed as is, or imported into other contracts to easily build 6 | cw721-compatible NFTs with custom logic. 7 | 8 | Implements: 9 | 10 | - [x] CW721 Base 11 | - [x] Metadata extension 12 | - [ ] Enumerable extension (AllTokens done, but not Tokens - requires [#81](https://github.com/CosmWasm/cosmwasm-plus/issues/81)) 13 | 14 | ## Implementation 15 | 16 | The `HandleMsg` and `QueryMsg` implementations follow the [CW721 spec](../../packages/cw721/README.md) and are described there. 17 | Beyond that, we make a few additions: 18 | 19 | * `InitMsg` takes name and symbol (for metadata), as well as a **Minter** address. This is a special address that has full 20 | power to mint new NFTs (but not modify existing ones) 21 | * `HandleMsg::Mint{token_id, owner, name, description, image}` - creates a new token with given owner and metadata. It can only be called by 22 | the Minter set in `init`. 23 | * `QueryMsg::Minter{}` - returns the minter address for this contract. 24 | 25 | It requires all tokens to have defined metadata in the standard format (with no extensions). For generic NFTs this may 26 | often be enough. 27 | 28 | The *Minter* can either be an external actor (eg. web server, using PubKey) or another contract. If you just want to customize 29 | the minting behavior but not other functionality, you could extend this contract (importing code and wiring it together) 30 | or just create a custom contract as the owner and use that contract to Mint. 31 | 32 | ## Running this contract 33 | 34 | You will need Rust 1.44.1+ with `wasm32-unknown-unknown` target installed. 35 | 36 | You can run unit tests on this via: 37 | 38 | `cargo test` 39 | 40 | Once you are happy with the content, you can compile it to wasm via: 41 | 42 | ``` 43 | RUSTFLAGS='-C link-arg=-s' cargo wasm 44 | cp ../../target/wasm32-unknown-unknown/release/cw20_base.wasm . 45 | ls -l cw20_base.wasm 46 | sha256sum cw20_base.wasm 47 | ``` 48 | 49 | Or for a production-ready (optimized) build, run a build command in the 50 | the repository root: https://github.com/CosmWasm/cosmwasm-plus#compiling. 51 | 52 | ## Importing this contract 53 | 54 | You can also import much of the logic of this contract to build another 55 | CW721-compliant contract, such as tradable names, crypto kitties, 56 | or tokenized real estate. 57 | 58 | Basically, you just need to write your handle function and import 59 | `cosmons::contract::handle_transfer`, etc and dispatch to them. 60 | This allows you to use custom `HandleMsg` and `QueryMsg` with your additional 61 | calls, but then use the underlying implementation for the standard cw721 62 | messages you want to support. The same with `QueryMsg`. You will most 63 | likely want to write a custom, domain-specific `init`. 64 | 65 | **TODO: add example when written** 66 | 67 | For now, you can look at [`cw20-staking`](../cw20-staking/README.md) 68 | for an example of how to "inherit" cw20 functionality and combine it with custom logic. 69 | The process is similar for cw721. 70 | -------------------------------------------------------------------------------- /contracts/cosmons/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use cosmons::package::{AllNftInfoResponse, NftInfoResponse}; 2 | use std::env::current_dir; 3 | use std::fs::create_dir_all; 4 | 5 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 6 | 7 | use cosmons::msg::{HandleMsg, InitMsg, MinterResponse, QueryMsg}; 8 | use cw721::{ 9 | ApprovedForAllResponse, ContractInfoResponse, NumTokensResponse, OwnerOfResponse, 10 | TokensResponse, 11 | }; 12 | 13 | fn main() { 14 | let mut out_dir = current_dir().unwrap(); 15 | out_dir.push("schema"); 16 | create_dir_all(&out_dir).unwrap(); 17 | remove_schemas(&out_dir).unwrap(); 18 | 19 | export_schema(&schema_for!(InitMsg), &out_dir); 20 | export_schema(&schema_for!(HandleMsg), &out_dir); 21 | export_schema(&schema_for!(QueryMsg), &out_dir); 22 | export_schema(&schema_for!(AllNftInfoResponse), &out_dir); 23 | export_schema(&schema_for!(ApprovedForAllResponse), &out_dir); 24 | export_schema(&schema_for!(ContractInfoResponse), &out_dir); 25 | export_schema(&schema_for!(MinterResponse), &out_dir); 26 | export_schema(&schema_for!(NftInfoResponse), &out_dir); 27 | export_schema(&schema_for!(NumTokensResponse), &out_dir); 28 | export_schema(&schema_for!(OwnerOfResponse), &out_dir); 29 | export_schema(&schema_for!(TokensResponse), &out_dir); 30 | } 31 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/all_nft_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AllNftInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "access", 7 | "info" 8 | ], 9 | "properties": { 10 | "access": { 11 | "description": "Who can transfer the token", 12 | "allOf": [ 13 | { 14 | "$ref": "#/definitions/OwnerOfResponse" 15 | } 16 | ] 17 | }, 18 | "info": { 19 | "description": "Data on the token itself,", 20 | "allOf": [ 21 | { 22 | "$ref": "#/definitions/NftInfoResponse" 23 | } 24 | ] 25 | } 26 | }, 27 | "definitions": { 28 | "Approval": { 29 | "type": "object", 30 | "required": [ 31 | "expires", 32 | "spender" 33 | ], 34 | "properties": { 35 | "expires": { 36 | "description": "When the Approval expires (maybe Expiration::never)", 37 | "allOf": [ 38 | { 39 | "$ref": "#/definitions/Expiration" 40 | } 41 | ] 42 | }, 43 | "spender": { 44 | "description": "Account that can transfer/send the token", 45 | "allOf": [ 46 | { 47 | "$ref": "#/definitions/HumanAddr" 48 | } 49 | ] 50 | } 51 | } 52 | }, 53 | "Expiration": { 54 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 55 | "anyOf": [ 56 | { 57 | "description": "AtHeight will expire when `env.block.height` >= height", 58 | "type": "object", 59 | "required": [ 60 | "at_height" 61 | ], 62 | "properties": { 63 | "at_height": { 64 | "type": "integer", 65 | "format": "uint64", 66 | "minimum": 0.0 67 | } 68 | } 69 | }, 70 | { 71 | "description": "AtTime will expire when `env.block.time` >= time", 72 | "type": "object", 73 | "required": [ 74 | "at_time" 75 | ], 76 | "properties": { 77 | "at_time": { 78 | "type": "integer", 79 | "format": "uint64", 80 | "minimum": 0.0 81 | } 82 | } 83 | }, 84 | { 85 | "description": "Never will never expire. Used to express the empty variant", 86 | "type": "object", 87 | "required": [ 88 | "never" 89 | ], 90 | "properties": { 91 | "never": { 92 | "type": "object" 93 | } 94 | } 95 | } 96 | ] 97 | }, 98 | "HumanAddr": { 99 | "type": "string" 100 | }, 101 | "NftInfoResponse": { 102 | "type": "object", 103 | "required": [ 104 | "description", 105 | "level", 106 | "name" 107 | ], 108 | "properties": { 109 | "description": { 110 | "description": "Describes the asset to which this NFT represents", 111 | "type": "string" 112 | }, 113 | "image": { 114 | "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety", 115 | "type": [ 116 | "string", 117 | "null" 118 | ] 119 | }, 120 | "level": { 121 | "description": "Describes Monster level", 122 | "type": "integer", 123 | "format": "uint64", 124 | "minimum": 0.0 125 | }, 126 | "name": { 127 | "description": "Identifies the asset to which this NFT represents", 128 | "type": "string" 129 | } 130 | } 131 | }, 132 | "OwnerOfResponse": { 133 | "type": "object", 134 | "required": [ 135 | "approvals", 136 | "owner" 137 | ], 138 | "properties": { 139 | "approvals": { 140 | "description": "If set this address is approved to transfer/send the token as well", 141 | "type": "array", 142 | "items": { 143 | "$ref": "#/definitions/Approval" 144 | } 145 | }, 146 | "owner": { 147 | "description": "Owner of the token", 148 | "allOf": [ 149 | { 150 | "$ref": "#/definitions/HumanAddr" 151 | } 152 | ] 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/approved_for_all_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ApprovedForAllResponse", 4 | "type": "object", 5 | "required": [ 6 | "operators" 7 | ], 8 | "properties": { 9 | "operators": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/Approval" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "Approval": { 18 | "type": "object", 19 | "required": [ 20 | "expires", 21 | "spender" 22 | ], 23 | "properties": { 24 | "expires": { 25 | "description": "When the Approval expires (maybe Expiration::never)", 26 | "allOf": [ 27 | { 28 | "$ref": "#/definitions/Expiration" 29 | } 30 | ] 31 | }, 32 | "spender": { 33 | "description": "Account that can transfer/send the token", 34 | "allOf": [ 35 | { 36 | "$ref": "#/definitions/HumanAddr" 37 | } 38 | ] 39 | } 40 | } 41 | }, 42 | "Expiration": { 43 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 44 | "anyOf": [ 45 | { 46 | "description": "AtHeight will expire when `env.block.height` >= height", 47 | "type": "object", 48 | "required": [ 49 | "at_height" 50 | ], 51 | "properties": { 52 | "at_height": { 53 | "type": "integer", 54 | "format": "uint64", 55 | "minimum": 0.0 56 | } 57 | } 58 | }, 59 | { 60 | "description": "AtTime will expire when `env.block.time` >= time", 61 | "type": "object", 62 | "required": [ 63 | "at_time" 64 | ], 65 | "properties": { 66 | "at_time": { 67 | "type": "integer", 68 | "format": "uint64", 69 | "minimum": 0.0 70 | } 71 | } 72 | }, 73 | { 74 | "description": "Never will never expire. Used to express the empty variant", 75 | "type": "object", 76 | "required": [ 77 | "never" 78 | ], 79 | "properties": { 80 | "never": { 81 | "type": "object" 82 | } 83 | } 84 | } 85 | ] 86 | }, 87 | "HumanAddr": { 88 | "type": "string" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/contract_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ContractInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "name", 7 | "symbol" 8 | ], 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "symbol": { 14 | "type": "string" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/init_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "InitMsg", 4 | "type": "object", 5 | "required": [ 6 | "minter", 7 | "name", 8 | "symbol" 9 | ], 10 | "properties": { 11 | "minter": { 12 | "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", 13 | "allOf": [ 14 | { 15 | "$ref": "#/definitions/HumanAddr" 16 | } 17 | ] 18 | }, 19 | "name": { 20 | "description": "name of the NFT contract", 21 | "type": "string" 22 | }, 23 | "symbol": { 24 | "description": "symbol of the NFT contract", 25 | "type": "string" 26 | } 27 | }, 28 | "definitions": { 29 | "HumanAddr": { 30 | "type": "string" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/minter_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "MinterResponse", 4 | "description": "Shows who can mint these tokens", 5 | "type": "object", 6 | "required": [ 7 | "minter" 8 | ], 9 | "properties": { 10 | "minter": { 11 | "$ref": "#/definitions/HumanAddr" 12 | } 13 | }, 14 | "definitions": { 15 | "HumanAddr": { 16 | "type": "string" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/nft_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "NftInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "description", 7 | "level", 8 | "name" 9 | ], 10 | "properties": { 11 | "description": { 12 | "description": "Describes the asset to which this NFT represents", 13 | "type": "string" 14 | }, 15 | "image": { 16 | "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety", 17 | "type": [ 18 | "string", 19 | "null" 20 | ] 21 | }, 22 | "level": { 23 | "description": "Describes Monster level", 24 | "type": "integer", 25 | "format": "uint64", 26 | "minimum": 0.0 27 | }, 28 | "name": { 29 | "description": "Identifies the asset to which this NFT represents", 30 | "type": "string" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/num_tokens_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "NumTokensResponse", 4 | "type": "object", 5 | "required": [ 6 | "count" 7 | ], 8 | "properties": { 9 | "count": { 10 | "type": "integer", 11 | "format": "uint64", 12 | "minimum": 0.0 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/owner_of_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "OwnerOfResponse", 4 | "type": "object", 5 | "required": [ 6 | "approvals", 7 | "owner" 8 | ], 9 | "properties": { 10 | "approvals": { 11 | "description": "If set this address is approved to transfer/send the token as well", 12 | "type": "array", 13 | "items": { 14 | "$ref": "#/definitions/Approval" 15 | } 16 | }, 17 | "owner": { 18 | "description": "Owner of the token", 19 | "allOf": [ 20 | { 21 | "$ref": "#/definitions/HumanAddr" 22 | } 23 | ] 24 | } 25 | }, 26 | "definitions": { 27 | "Approval": { 28 | "type": "object", 29 | "required": [ 30 | "expires", 31 | "spender" 32 | ], 33 | "properties": { 34 | "expires": { 35 | "description": "When the Approval expires (maybe Expiration::never)", 36 | "allOf": [ 37 | { 38 | "$ref": "#/definitions/Expiration" 39 | } 40 | ] 41 | }, 42 | "spender": { 43 | "description": "Account that can transfer/send the token", 44 | "allOf": [ 45 | { 46 | "$ref": "#/definitions/HumanAddr" 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | "Expiration": { 53 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 54 | "anyOf": [ 55 | { 56 | "description": "AtHeight will expire when `env.block.height` >= height", 57 | "type": "object", 58 | "required": [ 59 | "at_height" 60 | ], 61 | "properties": { 62 | "at_height": { 63 | "type": "integer", 64 | "format": "uint64", 65 | "minimum": 0.0 66 | } 67 | } 68 | }, 69 | { 70 | "description": "AtTime will expire when `env.block.time` >= time", 71 | "type": "object", 72 | "required": [ 73 | "at_time" 74 | ], 75 | "properties": { 76 | "at_time": { 77 | "type": "integer", 78 | "format": "uint64", 79 | "minimum": 0.0 80 | } 81 | } 82 | }, 83 | { 84 | "description": "Never will never expire. Used to express the empty variant", 85 | "type": "object", 86 | "required": [ 87 | "never" 88 | ], 89 | "properties": { 90 | "never": { 91 | "type": "object" 92 | } 93 | } 94 | } 95 | ] 96 | }, 97 | "HumanAddr": { 98 | "type": "string" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /contracts/cosmons/schema/tokens_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "TokensResponse", 4 | "type": "object", 5 | "required": [ 6 | "tokens" 7 | ], 8 | "properties": { 9 | "tokens": { 10 | "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", 11 | "type": "array", 12 | "items": { 13 | "type": "string" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contracts/cosmons/script.json: -------------------------------------------------------------------------------- 1 | wasmcli query wasm contract-state smart cosmos1kfz3mj84atqjld0ge9eccujvqqkqdr4qqs9ud7 '{ "balance": { "address": "cosmos1sm0hkulk4nufzq7dszdgv08xns3v8575h5g9ug" } }' 2 | 3 | cosmos1kfz3mj84atqjld0ge9eccujvqqkqdr4qqs9ud7 (contractAddress20) 4 | 5 | wasmcli tx wasm execute cosmos1g9vtch4xc9pl0fq2586mapzyjaw2yvv9xlcs6j '{ 6 | "send_nft": { 7 | "contract": "cosmos1knqr4zclds5zhn5khkpexkd7nctwe8z0s2qer4", 8 | "token_id": "monster112a9lf95atqvyejqe22xnna8x4mfqd75tkq2kvwcjyysarcsb", 9 | "msg": "eyJsaXN0X3ByaWNlIjp7ImFkZHJlc3MiOiJjb3Ntb3Mxa2Z6M21qODRhdHFqbGQwZ2U5ZWNjdWp2cXFrcWRyNHFxczl1ZDciLCJhbW91bnQiOiI5OSJ9fQ" 10 | } 11 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from cw721 12 | 13 | 14 | wasmcli query wasm contract-state smart cosmos1knqr4zclds5zhn5khkpexkd7nctwe8z0s2qer4 '{ 15 | "get_offerings": {} 16 | }' 17 | 18 | wasmcli tx wasm execute cosmos1knqr4zclds5zhn5khkpexkd7nctwe8z0s2qer4 '{ 19 | "withdraw_nft": { 20 | "offering_id": "4" 21 | } 22 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from cw721 23 | 24 | wasmcli tx wasm execute cosmos1knqr4zclds5zhn5khkpexkd7nctwe8z0s2qer4 '{ 25 | "withdraw_nft": { 26 | "offering_id": "4" 27 | } 28 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from cw721 -------------------------------------------------------------------------------- /contracts/cosmons/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("Unauthorized")] 10 | Unauthorized {}, 11 | 12 | #[error("token_id already claimed")] 13 | Claimed {}, 14 | 15 | #[error("Cannot set approval that is already expired")] 16 | Expired {}, 17 | } 18 | -------------------------------------------------------------------------------- /contracts/cosmons/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | pub mod msg; 4 | pub mod package; 5 | pub mod state; 6 | 7 | #[cfg(all(target_arch = "wasm32", not(feature = "library")))] 8 | cosmwasm_std::create_entry_points!(contract); 9 | -------------------------------------------------------------------------------- /contracts/cosmons/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Binary, HumanAddr}; 2 | use cw721::Expiration; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 7 | pub struct InitMsg { 8 | /// name of the NFT contract 9 | pub name: String, 10 | /// symbol of the NFT contract 11 | pub symbol: String, 12 | 13 | /// The minter is the only one who can create new NFTs. 14 | /// This is designed for a base NFT that is controlled by an external program 15 | /// or contract. You will likely replace this with custom logic in custom NFTs 16 | pub minter: HumanAddr, 17 | } 18 | 19 | /// This is like Cw721HandleMsg but we add a Mint command for an owner 20 | /// to make this stand-alone. You will likely want to remove mint and 21 | /// use other control logic in any contract that inherits this. 22 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 23 | #[serde(rename_all = "snake_case")] 24 | pub enum HandleMsg { 25 | /// Transfer is a base message to move a token to another account without triggering actions 26 | TransferNft { 27 | recipient: HumanAddr, 28 | token_id: String, 29 | }, 30 | /// Send is a base message to transfer a token to a contract and trigger an action 31 | /// on the receiving contract. 32 | SendNft { 33 | contract: HumanAddr, 34 | token_id: String, 35 | msg: Option, 36 | }, 37 | BattleMonster { 38 | attacker_id: String, 39 | defender_id: String, 40 | }, 41 | /// Allows operator to transfer / send the token from the owner's account. 42 | /// If expiration is set, then this allowance has a time/height limit 43 | Approve { 44 | spender: HumanAddr, 45 | token_id: String, 46 | expires: Option, 47 | }, 48 | /// Remove previously granted Approval 49 | Revoke { 50 | spender: HumanAddr, 51 | token_id: String, 52 | }, 53 | /// Allows operator to transfer / send any token from the owner's account. 54 | /// If expiration is set, then this allowance has a time/height limit 55 | ApproveAll { 56 | operator: HumanAddr, 57 | expires: Option, 58 | }, 59 | /// Remove previously granted ApproveAll permission 60 | RevokeAll { operator: HumanAddr }, 61 | 62 | /// Mint a new NFT, can only be called by the contract minter 63 | Mint(MintMsg), 64 | } 65 | 66 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 67 | pub struct MintMsg { 68 | /// Unique ID of the NFT 69 | pub token_id: String, 70 | /// The owner of the newly minter NFT 71 | pub owner: HumanAddr, 72 | /// Identifies the asset to which this NFT represents 73 | pub name: String, 74 | // Monster level 75 | pub level: u64, 76 | /// Describes the asset to which this NFT represents (may be empty) 77 | pub description: Option, 78 | /// A URI pointing to an image representing the asset 79 | pub image: Option, 80 | } 81 | 82 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 83 | #[serde(rename_all = "snake_case")] 84 | pub enum QueryMsg { 85 | /// Return the owner of the given token, error if token does not exist 86 | /// Return type: OwnerOfResponse 87 | OwnerOf { 88 | token_id: String, 89 | /// unset or false will filter out expired approvals, you must set to true to see them 90 | include_expired: Option, 91 | }, 92 | /// List all operators that can access all of the owner's tokens 93 | /// Return type: `ApprovedForAllResponse` 94 | ApprovedForAll { 95 | owner: HumanAddr, 96 | /// unset or false will filter out expired items, you must set to true to see them 97 | include_expired: Option, 98 | start_after: Option, 99 | limit: Option, 100 | }, 101 | /// Total number of tokens issued 102 | NumTokens {}, 103 | 104 | /// With MetaData Extension. 105 | /// Returns top-level metadata about the contract: `ContractInfoResponse` 106 | ContractInfo {}, 107 | /// With MetaData Extension. 108 | /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* 109 | /// but directly from the contract: `NftInfoResponse` 110 | NftInfo { 111 | token_id: String, 112 | }, 113 | /// With MetaData Extension. 114 | /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization 115 | /// for clients: `AllNftInfo` 116 | AllNftInfo { 117 | token_id: String, 118 | /// unset or false will filter out expired approvals, you must set to true to see them 119 | include_expired: Option, 120 | }, 121 | 122 | /// With Enumerable extension. 123 | /// Returns all tokens owned by the given address, [] if unset. 124 | /// Return type: TokensResponse. 125 | Tokens { 126 | owner: HumanAddr, 127 | start_after: Option, 128 | limit: Option, 129 | }, 130 | /// With Enumerable extension. 131 | /// Requires pagination. Lists all token_ids controlled by the contract. 132 | /// Return type: TokensResponse. 133 | AllTokens { 134 | start_after: Option, 135 | limit: Option, 136 | }, 137 | 138 | // Return the minter 139 | Minter {}, 140 | } 141 | 142 | /// Shows who can mint these tokens 143 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 144 | pub struct MinterResponse { 145 | pub minter: HumanAddr, 146 | } 147 | -------------------------------------------------------------------------------- /contracts/cosmons/src/package.rs: -------------------------------------------------------------------------------- 1 | use cw721::OwnerOfResponse; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 6 | pub struct NftInfoResponse { 7 | /// Identifies the asset to which this NFT represents 8 | pub name: String, 9 | /// Describes Monster level 10 | pub level: u64, 11 | /// Describes the asset to which this NFT represents 12 | pub description: String, 13 | /// "A URI pointing to a resource with mime type image/* representing the asset to which this 14 | /// NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect 15 | /// ratio between 1.91:1 and 4:5 inclusive. 16 | /// TODO: Use https://docs.rs/url_serde for type-safety 17 | pub image: Option, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 21 | pub struct AllNftInfoResponse { 22 | /// Who can transfer the token 23 | pub access: OwnerOfResponse, 24 | /// Data on the token itself, 25 | pub info: NftInfoResponse, 26 | } 27 | -------------------------------------------------------------------------------- /contracts/cosmons/src/state.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{CanonicalAddr, StdResult, Storage}; 5 | use cw721::{ContractInfoResponse, Expiration}; 6 | use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; 7 | 8 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 9 | pub struct TokenInfo { 10 | /// The owner of the newly minter NFT 11 | pub owner: CanonicalAddr, 12 | /// approvals are stored here, as we clear them all upon transfer and cannot accumulate much 13 | pub approvals: Vec, 14 | 15 | /// Identifies the asset to which this NFT represents 16 | pub name: String, 17 | 18 | //Add monster level 19 | pub level: u64, 20 | /// Describes the asset to which this NFT represents 21 | pub description: String, 22 | /// A URI pointing to an image representing the asset 23 | pub image: Option, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 27 | pub struct Approval { 28 | /// Account that can transfer/send the token 29 | pub spender: CanonicalAddr, 30 | /// When the Approval expires (maybe Expiration::never) 31 | pub expires: Expiration, 32 | } 33 | 34 | pub const CONTRACT_INFO: Item = Item::new(b"nft_info"); 35 | pub const MINTER: Item = Item::new(b"minter"); 36 | pub const TOKEN_COUNT: Item = Item::new(b"num_tokens"); 37 | 38 | // pub const TOKENS: Map<&str, TokenInfo> = Map::new(b"tokens"); 39 | pub const OPERATORS: Map<(&[u8], &[u8]), Expiration> = Map::new(b"operators"); 40 | 41 | pub fn num_tokens(storage: &S) -> StdResult { 42 | Ok(TOKEN_COUNT.may_load(storage)?.unwrap_or_default()) 43 | } 44 | 45 | pub fn increment_tokens(storage: &mut S) -> StdResult { 46 | let val = num_tokens(storage)? + 1; 47 | TOKEN_COUNT.save(storage, &val)?; 48 | Ok(val) 49 | } 50 | 51 | pub struct TokenIndexes<'a, S: Storage> { 52 | pub owner: MultiIndex<'a, S, TokenInfo>, 53 | } 54 | 55 | impl<'a, S: Storage> IndexList for TokenIndexes<'a, S> { 56 | fn get_indexes(&'_ self) -> Box> + '_> { 57 | let v: Vec<&dyn Index> = vec![&self.owner]; 58 | Box::new(v.into_iter()) 59 | } 60 | } 61 | 62 | pub fn tokens<'a, S: Storage>() -> IndexedMap<'a, &'a str, TokenInfo, S, TokenIndexes<'a, S>> { 63 | let indexes = TokenIndexes { 64 | owner: MultiIndex::new(|d| d.owner.to_vec(), b"tokens", b"tokens__owner"), 65 | }; 66 | IndexedMap::new(b"tokens", indexes) 67 | } 68 | -------------------------------------------------------------------------------- /contracts/marketplace/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | unit-test = "test --lib --features backtraces" 5 | schema = "run --example schema" 6 | -------------------------------------------------------------------------------- /contracts/marketplace/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "marketplace" 3 | version = "0.1.0" 4 | authors = ["blockscape "] 5 | edition = "2018" 6 | 7 | exclude = [ 8 | # 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. 9 | "contract.wasm", 10 | "hash.txt", 11 | ] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [profile.release] 19 | opt-level = 3 20 | debug = false 21 | rpath = false 22 | lto = true 23 | debug-assertions = false 24 | codegen-units = 1 25 | panic = 'abort' 26 | incremental = false 27 | overflow-checks = true 28 | 29 | [features] 30 | # for more explicit tests, cargo test --features=backtraces 31 | backtraces = ["cosmwasm-std/backtraces"] 32 | 33 | [dependencies] 34 | cosmwasm-std = { version = "0.11.0" } 35 | cw-storage-plus = {version = "0.3.2", features = ["iterator"]} 36 | cw20 = {version = "0.3.2"} 37 | cw721 = { path = "../../packages/cw721", version = "0.3.1"} 38 | schemars = "0.7" 39 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 40 | thiserror = { version = "1.0.21" } 41 | 42 | [dev-dependencies] 43 | 44 | cosmwasm-schema = { version = "0.11.0" } -------------------------------------------------------------------------------- /contracts/marketplace/HOWTO.md: -------------------------------------------------------------------------------- 1 | # How To 2 | 3 | This is a guide on how to setup your own set of contracts on the hackatom-wasm chain in conjunction with the marketplace. If you want to deploy the contracts on a local chain, please follow the [CosmWasm docs](https://docs.cosmwasm.com/getting-started/setting-env.html#run-local-node-optional) for instructions on how to set up a local node. 4 | 5 | You will need two accounts with some tokens from the [faucet](https://five.hackatom.org/resources). Otherwise, you won't be able to upload the smart contracts to the hackatom-wasm chain. 6 | 7 | > :information_source: **If you already have accounts with funds, you can skip this step.** 8 | 9 | ```shell 10 | # Create accounts and save mnemonics 11 | wasmcli keys add client 12 | wasmcli keys add partner 13 | ``` 14 | 15 | ## Building the Contracts 16 | 17 | We need to build three smart contracts in total: 18 | 19 | * `cw20-base` for buying tokens, 20 | * `cw721-base` for selling tokens and withdrawing offerings 21 | * and `marketplace`. 22 | 23 | ```shell 24 | # Make sure all of your contracts actually build using 25 | cargo wasm 26 | 27 | # Switch into the hackatom_v/ directory and use the workspace-optimizer v0.10.4 to build your contracts 28 | docker run --rm -v "$(pwd)":/code \ 29 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ 30 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 31 | cosmwasm/workspace-optimizer:0.10.4 32 | ``` 33 | 34 | Once the workspace-optimizer is done, the `.wasm` files will be put into the `artifacts/` directory. 35 | 36 | ## Uploading the Contracts 37 | 38 | Now that we've built our contracts, we need to upload them to the blockchain. 39 | 40 | > :information_source: In order to avoid confusion, run `wasmcli query wasm list-code` after each individual upload to get the contract ID. You will be needing the IDs in the next step. 41 | 42 | ```shell 43 | wasmcli tx wasm store artifacts/cw20_base.wasm --from client --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y 44 | wasmcli tx wasm store artifacts/cw721_base.wasm --from client --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y 45 | wasmcli tx wasm store artifacts/marketplace.wasm --from client --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y 46 | ``` 47 | 48 | ## Instantiating the Contracts 49 | 50 | Now that we've uploaded our contracts to the blockchain, we need to instantiate them individually using the IDs we got from uploading. 51 | 52 | ```shell 53 | # cw20-base initialization 54 | # - name: Your custom name of your CW20 contract 55 | # - symbol: All upper case symbol of your CW20 token, must be 3-6 characters long (i.e. ATOM) 56 | # - decimals: Number of decimal places for your tokens (i.e. 3 -> 1.xxx ATOM) 57 | # - initial balances: Array with one or more accounts to give some tokens on contract initialization 58 | # - address: Account address to receive tokens 59 | # - amount: Amount of tokens the address should receive 60 | # - mint: Object holding the minter address 61 | # - minter: Account address of the CW20 token minter 62 | wasmcli tx wasm instantiate '{ 63 | "name": "", 64 | "symbol": "", 65 | "decimals": , 66 | "initial_balances": [ 67 | { 68 | "address": "", 69 | "amount": "" 70 | } 71 | ], 72 | "mint": { 73 | "minter": "" 74 | } 75 | }' --label "cw20-base" --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 76 | 77 | # cw721-base initialization 78 | # - name: Your custom name of your CW721 contract 79 | # - symbol: All upper case symbol of your CW721 token, must be 3-6 characters long (i.e. ATOM) 80 | # - minter: Account address of the CW721 token minter 81 | wasmcli tx wasm instantiate '{ 82 | "name": "", 83 | "symbol": "", 84 | "minter": "" 85 | }' --label "cw721-base" --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 86 | 87 | # marketplace initialization 88 | # - name: Your custom name of your marketplace contract 89 | wasmcli tx wasm instantiate '{ 90 | "name": "" 91 | }' --label "marketplace" --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 92 | ``` 93 | 94 | Once instantiated, you can use `wasmcli query wasm list-contract-by-code ` to query contract info. 95 | -------------------------------------------------------------------------------- /contracts/marketplace/README.md: -------------------------------------------------------------------------------- 1 | # Marketplace Smart Contract 2 | 3 | The marketplace smart contracts provides a generic platform used for selling and buying CW721 tokens with CW20 tokens. It maintains a list of all current offerings, including the seller's address, the token ID put up for sale, the list price of the token and the contract address the offerings originated from. This ensures maximum visibility on a per-sale instead of a per-contract basis, allowing users to browse through list of offerings in one central place. 4 | 5 | ## Requirements 6 | 7 | * [Go `v1.14+`](https://golang.org/) 8 | * [Rust `v1.44.1+`](https://rustup.rs/) 9 | * [Wasmd v0.11.1](https://github.com/CosmWasm/wasmd/tree/v0.11.1) 10 | * [cosmwasm-plus v0.3.2](https://github.com/CosmWasm/cosmwasm-plus) 11 | * [cw20-base](https://github.com/CosmWasm/cosmwasm-plus/tree/master/contracts/cw20-base) 12 | * [cosmons](https://github.com/BlockscapeNetwork/hackatom_v/tree/master/contracts/cosmons) 13 | 14 | ## Setup Environment 15 | 16 | 1) Follow [the CosmWasm docs](https://docs.cosmwasm.com/getting-started/installation.html) to install `go v1.14+`, `rust v1.44.1+` and `wasmd v0.11.1` 17 | 2) Once you've built `wasmd`, use the `wasmcli` to join the `hackatom-wasm` chain. 18 | 19 | > :information_source: If you want to deploy your own contracts on your own chain, check out the [HOWTO](HOWTO.md). 20 | 21 | ```shell 22 | wasmcli config chain-id hackatom-wasm 23 | wasmcli config indent true 24 | wasmcli config keyring-backend test 25 | wasmcli config node https://rpc.cosmwasm.hub.hackatom.dev:443 26 | wasmcli config output json 27 | wasmcli config trust-node true 28 | ``` 29 | 30 | 3) Create an account with some tokens from the [faucet](https://five.hackatom.org/resources). Otherwise, you won't be able to make any transactions. 31 | 32 | > :information_source: **If you already have an account with funds, you can skip this step.** 33 | 34 | ```shell 35 | # Create account and save mnemonics 36 | wasmcli keys add myacc 37 | ``` 38 | 39 | 4) Before you can buy or sell CW721 tokens, you will need some CW20 tokens. You can get them from our faucet: `POST 3.121.232.142:8080/faucet` 40 | 41 | Example payload: 42 | 43 | ```json 44 | { 45 | "address": "" 46 | } 47 | ``` 48 | 49 | ## Contract Addresses 50 | 51 | | Contract | Address | 52 | |:----------------|:----------------------------------------------| 53 | | marketplace | cosmos1knqr4zclds5zhn5khkpexkd7nctwe8z0s2qer4 | 54 | | cw20-base | cosmos1kfz3mj84atqjld0ge9eccujvqqkqdr4qqs9ud7 | 55 | | cosmons (cw721) | cosmos1zhh3m9sg5e2qvjgwr49r79pf5pt65yuxvs7cs0 | 56 | 57 | ## Messages 58 | 59 | ### Sell CW721 Token 60 | 61 | Puts an NFT token up for sale. 62 | 63 | > :warning: The seller needs to be the owner of the token to be able to sell it. 64 | 65 | ```shell 66 | # Execute send_nft action to put token up for sale for specified list_price on the marketplace 67 | wasmcli tx wasm execute '{ 68 | "send_nft": { 69 | "contract": "", 70 | "token_id": "", 71 | "msg": "BASE64_ENCODED_JSON --> { "list_price": { "address": "", "amount": "" }} <--" 72 | } 73 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 74 | ``` 75 | 76 | ### Withdraw CW721 Token Offering 77 | 78 | Withdraws an NFT token offering from the global offerings list and returns the NFT token back to its owner. 79 | 80 | > :warning: Only the token's owner/seller can withdraw the offering. This will only work after having used `sell_nft` on a token. 81 | 82 | ```shell 83 | # Execute withdraw_nft action to withdraw the token with the specified offering_id from the marketplace 84 | wasmcli tx wasm execute '{ 85 | "withdraw_nft": { 86 | "offering_id": "" 87 | } 88 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 89 | ``` 90 | 91 | ### Buy CW721 Token 92 | 93 | Buys an NFT token, transferring funds to the seller and the token to the buyer. 94 | 95 | > :warning: This will only work after having used `sell_nft` on a token. 96 | 97 | ```shell 98 | # Execute send action to buy token with the specified offering_id from the marketplace 99 | wasmcli tx wasm execute '{ 100 | "send": { 101 | "contract": "", 102 | "amount": "", 103 | "msg": "BASE64_ENCODED_JSON --> { "offering_id": "" } <--" 104 | } 105 | }' --gas-prices="0.025ucosm" --gas="auto" --gas-adjustment="1.2" -y --from client 106 | ``` 107 | 108 | ## Queries 109 | 110 | ### Query Offerings 111 | 112 | Retrieves a list of all currently listed offerings. 113 | 114 | ```shell 115 | wasmcli query wasm contract-state smart '{ 116 | "get_offerings": {} 117 | }' 118 | ``` 119 | -------------------------------------------------------------------------------- /contracts/marketplace/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | use marketplace::msg::{BuyNft, HandleMsg, InitMsg, QueryMsg, SellNft}; 7 | use marketplace::package::{ContractInfoResponse, OfferingsResponse, QueryOfferingsResult}; 8 | 9 | fn main() { 10 | let mut out_dir = current_dir().unwrap(); 11 | out_dir.push("schema"); 12 | create_dir_all(&out_dir).unwrap(); 13 | remove_schemas(&out_dir).unwrap(); 14 | 15 | export_schema(&schema_for!(InitMsg), &out_dir); 16 | export_schema(&schema_for!(HandleMsg), &out_dir); 17 | export_schema(&schema_for!(QueryMsg), &out_dir); 18 | export_schema(&schema_for!(SellNft), &out_dir); 19 | export_schema(&schema_for!(BuyNft), &out_dir); 20 | export_schema(&schema_for!(OfferingsResponse), &out_dir); 21 | export_schema(&schema_for!(ContractInfoResponse), &out_dir); 22 | export_schema(&schema_for!(QueryOfferingsResult), &out_dir); 23 | } 24 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/buy_nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "BuyNft", 4 | "type": "object", 5 | "required": [ 6 | "offering_id" 7 | ], 8 | "properties": { 9 | "offering_id": { 10 | "type": "string" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/contract_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ContractInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "name" 7 | ], 8 | "properties": { 9 | "name": { 10 | "type": "string" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/handle_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "HandleMsg", 4 | "anyOf": [ 5 | { 6 | "type": "object", 7 | "required": [ 8 | "withdraw_nft" 9 | ], 10 | "properties": { 11 | "withdraw_nft": { 12 | "type": "object", 13 | "required": [ 14 | "offering_id" 15 | ], 16 | "properties": { 17 | "offering_id": { 18 | "type": "string" 19 | } 20 | } 21 | } 22 | } 23 | }, 24 | { 25 | "type": "object", 26 | "required": [ 27 | "receive" 28 | ], 29 | "properties": { 30 | "receive": { 31 | "$ref": "#/definitions/Cw20ReceiveMsg" 32 | } 33 | } 34 | }, 35 | { 36 | "type": "object", 37 | "required": [ 38 | "receive_nft" 39 | ], 40 | "properties": { 41 | "receive_nft": { 42 | "$ref": "#/definitions/Cw721ReceiveMsg" 43 | } 44 | } 45 | } 46 | ], 47 | "definitions": { 48 | "Binary": { 49 | "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", 50 | "type": "string" 51 | }, 52 | "Cw20ReceiveMsg": { 53 | "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg", 54 | "type": "object", 55 | "required": [ 56 | "amount", 57 | "sender" 58 | ], 59 | "properties": { 60 | "amount": { 61 | "$ref": "#/definitions/Uint128" 62 | }, 63 | "msg": { 64 | "anyOf": [ 65 | { 66 | "$ref": "#/definitions/Binary" 67 | }, 68 | { 69 | "type": "null" 70 | } 71 | ] 72 | }, 73 | "sender": { 74 | "$ref": "#/definitions/HumanAddr" 75 | } 76 | } 77 | }, 78 | "Cw721ReceiveMsg": { 79 | "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg", 80 | "type": "object", 81 | "required": [ 82 | "sender", 83 | "token_id" 84 | ], 85 | "properties": { 86 | "msg": { 87 | "anyOf": [ 88 | { 89 | "$ref": "#/definitions/Binary" 90 | }, 91 | { 92 | "type": "null" 93 | } 94 | ] 95 | }, 96 | "sender": { 97 | "$ref": "#/definitions/HumanAddr" 98 | }, 99 | "token_id": { 100 | "type": "string" 101 | } 102 | } 103 | }, 104 | "HumanAddr": { 105 | "type": "string" 106 | }, 107 | "Uint128": { 108 | "type": "string" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/init_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "InitMsg", 4 | "type": "object", 5 | "required": [ 6 | "name" 7 | ], 8 | "properties": { 9 | "name": { 10 | "type": "string" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/offerings_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "OfferingsResponse", 4 | "type": "object", 5 | "required": [ 6 | "offerings" 7 | ], 8 | "properties": { 9 | "offerings": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/QueryOfferingsResult" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "Cw20CoinHuman": { 18 | "type": "object", 19 | "required": [ 20 | "address", 21 | "amount" 22 | ], 23 | "properties": { 24 | "address": { 25 | "$ref": "#/definitions/HumanAddr" 26 | }, 27 | "amount": { 28 | "$ref": "#/definitions/Uint128" 29 | } 30 | } 31 | }, 32 | "HumanAddr": { 33 | "type": "string" 34 | }, 35 | "QueryOfferingsResult": { 36 | "type": "object", 37 | "required": [ 38 | "contract_addr", 39 | "id", 40 | "list_price", 41 | "seller", 42 | "token_id" 43 | ], 44 | "properties": { 45 | "contract_addr": { 46 | "$ref": "#/definitions/HumanAddr" 47 | }, 48 | "id": { 49 | "type": "string" 50 | }, 51 | "list_price": { 52 | "$ref": "#/definitions/Cw20CoinHuman" 53 | }, 54 | "seller": { 55 | "$ref": "#/definitions/HumanAddr" 56 | }, 57 | "token_id": { 58 | "type": "string" 59 | } 60 | } 61 | }, 62 | "Uint128": { 63 | "type": "string" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/query_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "QueryMsg", 4 | "anyOf": [ 5 | { 6 | "type": "object", 7 | "required": [ 8 | "get_offerings" 9 | ], 10 | "properties": { 11 | "get_offerings": { 12 | "type": "object" 13 | } 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/query_offerings_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "QueryOfferingsResult", 4 | "type": "object", 5 | "required": [ 6 | "contract_addr", 7 | "id", 8 | "list_price", 9 | "seller", 10 | "token_id" 11 | ], 12 | "properties": { 13 | "contract_addr": { 14 | "$ref": "#/definitions/HumanAddr" 15 | }, 16 | "id": { 17 | "type": "string" 18 | }, 19 | "list_price": { 20 | "$ref": "#/definitions/Cw20CoinHuman" 21 | }, 22 | "seller": { 23 | "$ref": "#/definitions/HumanAddr" 24 | }, 25 | "token_id": { 26 | "type": "string" 27 | } 28 | }, 29 | "definitions": { 30 | "Cw20CoinHuman": { 31 | "type": "object", 32 | "required": [ 33 | "address", 34 | "amount" 35 | ], 36 | "properties": { 37 | "address": { 38 | "$ref": "#/definitions/HumanAddr" 39 | }, 40 | "amount": { 41 | "$ref": "#/definitions/Uint128" 42 | } 43 | } 44 | }, 45 | "HumanAddr": { 46 | "type": "string" 47 | }, 48 | "Uint128": { 49 | "type": "string" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /contracts/marketplace/schema/sell_nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "SellNft", 4 | "type": "object", 5 | "required": [ 6 | "list_price" 7 | ], 8 | "properties": { 9 | "list_price": { 10 | "$ref": "#/definitions/Cw20CoinHuman" 11 | } 12 | }, 13 | "definitions": { 14 | "Cw20CoinHuman": { 15 | "type": "object", 16 | "required": [ 17 | "address", 18 | "amount" 19 | ], 20 | "properties": { 21 | "address": { 22 | "$ref": "#/definitions/HumanAddr" 23 | }, 24 | "amount": { 25 | "$ref": "#/definitions/Uint128" 26 | } 27 | } 28 | }, 29 | "HumanAddr": { 30 | "type": "string" 31 | }, 32 | "Uint128": { 33 | "type": "string" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/marketplace/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::StdError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum ContractError { 6 | #[error("{0}")] 7 | Std(#[from] StdError), 8 | 9 | #[error("No data in ReceiveMsg")] 10 | NoData {}, 11 | 12 | #[error("Unauthorized")] 13 | Unauthorized {}, 14 | // Add any other custom errors you like here. 15 | // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. 16 | #[error("Insufficient funds")] 17 | InsufficientFunds {}, 18 | } 19 | -------------------------------------------------------------------------------- /contracts/marketplace/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod contract; 2 | mod error; 3 | pub mod msg; 4 | pub mod package; 5 | pub mod state; 6 | 7 | #[cfg(all(target_arch = "wasm32", not(feature = "library")))] 8 | cosmwasm_std::create_entry_points!(contract); 9 | -------------------------------------------------------------------------------- /contracts/marketplace/src/msg.rs: -------------------------------------------------------------------------------- 1 | use cw20::{Cw20CoinHuman, Cw20ReceiveMsg}; 2 | use cw721::Cw721ReceiveMsg; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 7 | pub struct InitMsg { 8 | pub name: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 12 | #[serde(rename_all = "snake_case")] 13 | pub enum HandleMsg { 14 | WithdrawNft { offering_id: String }, 15 | Receive(Cw20ReceiveMsg), 16 | ReceiveNft(Cw721ReceiveMsg), 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 20 | #[serde(rename_all = "snake_case")] 21 | pub struct SellNft { 22 | pub list_price: Cw20CoinHuman, 23 | } 24 | 25 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 26 | #[serde(rename_all = "snake_case")] 27 | pub struct BuyNft { 28 | pub offering_id: String, 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 32 | #[serde(rename_all = "snake_case")] 33 | pub enum QueryMsg { 34 | // GetOfferings returns a list of all offerings 35 | GetOfferings {}, 36 | } 37 | -------------------------------------------------------------------------------- /contracts/marketplace/src/package.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::HumanAddr; 2 | use cw20::Cw20CoinHuman; 3 | use schemars::JsonSchema; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 7 | pub struct ContractInfoResponse { 8 | pub name: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 12 | pub struct QueryOfferingsResult { 13 | pub id: String, 14 | pub token_id: String, 15 | pub list_price: Cw20CoinHuman, 16 | pub contract_addr: HumanAddr, 17 | pub seller: HumanAddr, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 21 | pub struct OfferingsResponse { 22 | pub offerings: Vec, 23 | } 24 | 25 | // THIS FILE SHOULD BE EXTRACTED TO ITS OWN PACKAGE PROJECT LIKE CW20 OR CW721 26 | -------------------------------------------------------------------------------- /contracts/marketplace/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::package::ContractInfoResponse; 2 | use schemars::JsonSchema; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use cosmwasm_std::{CanonicalAddr, StdResult, Storage}; 6 | use cw20::Cw20CoinHuman; 7 | use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; 8 | 9 | pub static CONFIG_KEY: &[u8] = b"config"; 10 | 11 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 12 | pub struct Offering { 13 | pub token_id: String, 14 | 15 | pub contract_addr: CanonicalAddr, 16 | 17 | pub seller: CanonicalAddr, 18 | 19 | pub list_price: Cw20CoinHuman, 20 | } 21 | 22 | /// OFFERINGS is a map which maps the offering_id to an offering. Offering_id is derived from OFFERINGS_COUNT. 23 | pub const OFFERINGS: Map<&str, Offering> = Map::new(b"offerings"); 24 | pub const OFFERINGS_COUNT: Item = Item::new(b"num_offerings"); 25 | pub const CONTRACT_INFO: Item = Item::new(b"marketplace_info"); 26 | 27 | pub fn num_offerings(storage: &S) -> StdResult { 28 | Ok(OFFERINGS_COUNT.may_load(storage)?.unwrap_or_default()) 29 | } 30 | 31 | pub fn increment_offerings(storage: &mut S) -> StdResult { 32 | let val = num_offerings(storage)? + 1; 33 | OFFERINGS_COUNT.save(storage, &val)?; 34 | Ok(val) 35 | } 36 | 37 | pub struct OfferingIndexes<'a, S: Storage> { 38 | pub seller: MultiIndex<'a, S, Offering>, 39 | pub contract: MultiIndex<'a, S, Offering>, 40 | } 41 | 42 | impl<'a, S: Storage> IndexList for OfferingIndexes<'a, S> { 43 | fn get_indexes(&'_ self) -> Box> + '_> { 44 | let v: Vec<&dyn Index> = vec![&self.seller, &self.contract]; 45 | Box::new(v.into_iter()) 46 | } 47 | } 48 | 49 | pub fn offerings<'a, S: Storage>() -> IndexedMap<'a, &'a str, Offering, S, OfferingIndexes<'a, S>> { 50 | let indexes = OfferingIndexes { 51 | seller: MultiIndex::new(|o| o.seller.to_vec(), b"offerings", b"offerings__seller"), 52 | contract: MultiIndex::new( 53 | |o| o.contract_addr.to_vec(), 54 | b"offerings", 55 | b"offerings__contract", 56 | ), 57 | }; 58 | IndexedMap::new(b"offerings", indexes) 59 | } 60 | -------------------------------------------------------------------------------- /packages/cw0/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw0" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Common helpers for other cw specs" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | cosmwasm-std = { version = "0.11.1" } 16 | schemars = "0.7" 17 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 18 | -------------------------------------------------------------------------------- /packages/cw0/NOTICE: -------------------------------------------------------------------------------- 1 | CW0: Common types and helpers for CosmWasm specs 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw0/README.md: -------------------------------------------------------------------------------- 1 | # CW0: Common Types for specs 2 | 3 | This is a collection of common types shared among many specs. 4 | For example `Expiration`, which is embedded in many places. 5 | 6 | Types should only be added here after they are duplicated in 7 | a second contract, not "because we might need it" 8 | -------------------------------------------------------------------------------- /packages/cw0/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod balance; 2 | mod expiration; 3 | mod pagination; 4 | 5 | pub use crate::balance::NativeBalance; 6 | pub use crate::expiration::{Duration, Expiration, DAY, HOUR, WEEK}; 7 | pub use pagination::{ 8 | calc_range_end_human, calc_range_start_human, calc_range_start_string, maybe_canonical, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/cw0/src/pagination.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{Api, CanonicalAddr, HumanAddr, StdResult}; 2 | 3 | // this is used for pagination. Maybe we move it into the std lib one day? 4 | pub fn maybe_canonical( 5 | api: A, 6 | human: Option, 7 | ) -> StdResult> { 8 | human.map(|x| api.canonical_address(&x)).transpose() 9 | } 10 | 11 | // this will set the first key after the provided key, by appending a 0 byte 12 | pub fn calc_range_start_human( 13 | api: A, 14 | start_after: Option, 15 | ) -> StdResult>> { 16 | match start_after { 17 | Some(human) => { 18 | let mut v: Vec = api.canonical_address(&human)?.0.into(); 19 | v.push(0); 20 | Ok(Some(v)) 21 | } 22 | None => Ok(None), 23 | } 24 | } 25 | 26 | // set the end to the canonicalized format (used for Order::Descending) 27 | pub fn calc_range_end_human( 28 | api: A, 29 | end_before: Option, 30 | ) -> StdResult>> { 31 | match end_before { 32 | Some(human) => { 33 | let v: Vec = api.canonical_address(&human)?.0.into(); 34 | Ok(Some(v)) 35 | } 36 | None => Ok(None), 37 | } 38 | } 39 | 40 | // this will set the first key after the provided key, by appending a 1 byte 41 | pub fn calc_range_start_string(start_after: Option) -> Option> { 42 | start_after.map(|token_id| { 43 | let mut v: Vec = token_id.into_bytes(); 44 | v.push(0); 45 | v 46 | }) 47 | } 48 | 49 | #[cfg(test)] 50 | mod test { 51 | // TODO: add unit tests 52 | #[ignore] 53 | #[test] 54 | fn add_some_tests() {} 55 | } 56 | -------------------------------------------------------------------------------- /packages/cw1/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | schema = "run --example schema" 5 | -------------------------------------------------------------------------------- /packages/cw1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw1" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Definition and types for the CosmWasm-1 interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cosmwasm-std = { version = "0.11.1" } 14 | schemars = "0.7" 15 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 16 | 17 | [dev-dependencies] 18 | cosmwasm-schema = { version = "0.11.1" } 19 | -------------------------------------------------------------------------------- /packages/cw1/NOTICE: -------------------------------------------------------------------------------- 1 | CW1: A CosmWasm spec for proxy contracts 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw1/README.md: -------------------------------------------------------------------------------- 1 | # CW1 Spec: Proxy Contracts 2 | 3 | CW1 is a specification for proxy contracts based on CosmWasm. 4 | It is a very simple, but flexible interface designed for the case 5 | where one contract is meant to hold assets (or rights) on behalf of 6 | other contracts. 7 | 8 | The simplest example is a contract that will accept messages from 9 | the creator and resend them from it's address. Simply by making this 10 | transferable, you can then begin to transfer non-transferable assets 11 | (eg. staked tokens, voting power, etc). 12 | 13 | You can imagine more complex examples, such as a "1 of N" multisig, 14 | or conditional approval, where "sub-accounts" have the right to spend 15 | a limited amount of funds from this account, with a "admin account" 16 | retaining full control. 17 | 18 | The common denominator is that they allow you to immediately 19 | execute arbitrary `CosmosMsg` in the same transaction. 20 | 21 | ### Messages 22 | 23 | `Execute{msgs}` - This accepts `Vec` and checks permissions 24 | before re-dispatching all those messages from the contract address. 25 | 26 | ### Queries 27 | 28 | `CanSend{sender, msg}` - This accepts one `CosmosMsg` and checks permissions, 29 | returning true or false based on the permissions. If `CanSend` returns true 30 | then a call to `Execute` from that sender, with the same message, 31 | before any further state changes, should also succeed. This can be used 32 | to dynamically provide some client info on a generic cw1 contract without 33 | knowing the extension details. (eg. detect if they can send coins or stake) -------------------------------------------------------------------------------- /packages/cw1/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; 5 | 6 | use cw1::{CanSendResponse, Cw1HandleMsg, Cw1QueryMsg}; 7 | 8 | fn main() { 9 | let mut out_dir = current_dir().unwrap(); 10 | out_dir.push("schema"); 11 | create_dir_all(&out_dir).unwrap(); 12 | remove_schemas(&out_dir).unwrap(); 13 | 14 | export_schema_with_title(&mut schema_for!(Cw1HandleMsg), &out_dir, "HandleMsg"); 15 | export_schema_with_title(&mut schema_for!(Cw1QueryMsg), &out_dir, "QueryMsg"); 16 | export_schema(&schema_for!(CanSendResponse), &out_dir); 17 | } 18 | -------------------------------------------------------------------------------- /packages/cw1/schema/can_send_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "CanSendResponse", 4 | "type": "object", 5 | "required": [ 6 | "can_send" 7 | ], 8 | "properties": { 9 | "can_send": { 10 | "type": "boolean" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/cw1/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, StdResult, WasmMsg}; 5 | 6 | use crate::msg::Cw1HandleMsg; 7 | 8 | /// Cw1Contract is a wrapper around HumanAddr that provides a lot of helpers 9 | /// for working with this. 10 | /// 11 | /// If you wish to persist this, convert to Cw1CanonicalContract via .canonical() 12 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 13 | pub struct Cw1Contract(pub HumanAddr); 14 | 15 | impl Cw1Contract { 16 | pub fn addr(&self) -> HumanAddr { 17 | self.0.clone() 18 | } 19 | 20 | /// Convert this address to a form fit for storage 21 | pub fn canonical(&self, api: &A) -> StdResult { 22 | let canon = api.canonical_address(&self.0)?; 23 | Ok(Cw1CanonicalContract(canon)) 24 | } 25 | 26 | pub fn execute>>(&self, msgs: T) -> StdResult { 27 | let msg = Cw1HandleMsg::Execute { msgs: msgs.into() }; 28 | Ok(WasmMsg::Execute { 29 | contract_addr: self.addr(), 30 | msg: to_binary(&msg)?, 31 | send: vec![], 32 | } 33 | .into()) 34 | } 35 | } 36 | 37 | /// This is a respresentation of Cw1Contract for storage. 38 | /// Don't use it directly, just translate to the Cw1Contract when needed. 39 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 40 | pub struct Cw1CanonicalContract(pub CanonicalAddr); 41 | 42 | impl Cw1CanonicalContract { 43 | /// Convert this address to a form fit for usage in messages and queries 44 | pub fn human(&self, api: &A) -> StdResult { 45 | let human = api.human_address(&self.0)?; 46 | Ok(Cw1Contract(human)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/cw1/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod helpers; 2 | pub mod msg; 3 | pub mod query; 4 | 5 | pub use crate::helpers::{Cw1CanonicalContract, Cw1Contract}; 6 | pub use crate::msg::Cw1HandleMsg; 7 | pub use crate::query::{CanSendResponse, Cw1QueryMsg}; 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | #[test] 12 | fn it_works() { 13 | assert_eq!(2 + 2, 4); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cw1/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_std::{CosmosMsg, Empty}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum Cw1HandleMsg 10 | where 11 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 12 | { 13 | /// Execute requests the contract to re-dispatch all these messages with the 14 | /// contract's address as sender. Every implementation has it's own logic to 15 | /// determine in 16 | Execute { msgs: Vec> }, 17 | } 18 | -------------------------------------------------------------------------------- /packages/cw1/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_std::{CosmosMsg, Empty, HumanAddr}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum Cw1QueryMsg 10 | where 11 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 12 | { 13 | /// Checks permissions of the caller on this proxy. 14 | /// If CanSend returns true then a call to `Execute` with the same message, 15 | /// from the given sender, before any further state changes, should also succeed. 16 | CanSend { 17 | sender: HumanAddr, 18 | msg: CosmosMsg, 19 | }, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 23 | pub struct CanSendResponse { 24 | pub can_send: bool, 25 | } 26 | -------------------------------------------------------------------------------- /packages/cw2/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | -------------------------------------------------------------------------------- /packages/cw2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw2" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Definition and types for the CosmWasm-2 interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cosmwasm-std = { version = "0.11.1" } 14 | cw-storage-plus = { path = "../../packages/storage-plus", version = "0.3.2" } 15 | schemars = "0.7" 16 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 17 | -------------------------------------------------------------------------------- /packages/cw2/NOTICE: -------------------------------------------------------------------------------- 1 | CW2: A CosmWasm spec for migration info 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw2/README.md: -------------------------------------------------------------------------------- 1 | # CW2 Spec: Contract Info for Migration 2 | 3 | Most of the CW* specs are focused on the *public interfaces* 4 | of the contract. The APIs used for `HandleMsg` or `QueryMsg`. 5 | However, when we wish to migrate from contract A to contract B, 6 | contract B needs to be aware somehow of how the *state was encoded*. 7 | 8 | Generally we use Singletons and Buckets to store the state, but 9 | if I upgrade to a `cw20-with-bonding-curve` contract, it will only 10 | work properly if I am migrating from a `cw20-base` contract. But how 11 | can the new contract know what format the data was stored. 12 | 13 | This is where CW2 comes in. It specifies on special Singleton to 14 | be stored on disk by all contracts on `init`. When the `migrate` 15 | function is called, then the new contract can read that data and 16 | see if this is an expected contract we can migrate from. And also 17 | contain extra version information if we support multiple migrate 18 | paths. 19 | 20 | ### Data structures 21 | 22 | **Required** 23 | 24 | All CW2-compliant contracts must store the following data: 25 | 26 | * key: `\x00\x0dcontract_info` (length prefixed "contract_info" using Singleton pattern) 27 | * data: Json-serialized `ContractVersion` 28 | 29 | ```rust 30 | pub struct ContractVersion { 31 | /// contract is a globally unique identifier for the contract. 32 | /// it should build off standard namespacing for whichever language it is in, 33 | /// and prefix it with the registry we use. 34 | /// For rust we prefix with `crates.io:`, to give us eg. `crates.io:cw20-base` 35 | pub contract: String, 36 | /// version is any string that this implementation knows. It may be simple counter "1", "2". 37 | /// or semantic version on release tags "v0.6.2", or some custom feature flag list. 38 | /// the only code that needs to understand the version parsing is code that knows how to 39 | /// migrate from the given contract (and is tied to it's implementation somehow) 40 | pub version: String, 41 | } 42 | ``` 43 | 44 | Thus, an serialized example may looks like: 45 | 46 | ```json 47 | { 48 | "contract": "crates.io:cw20-base", 49 | "version": "v0.1.0" 50 | } 51 | ``` 52 | 53 | ### Queries 54 | 55 | Since the state is well defined, we do not need to support any "smart queries". 56 | We do provide a helper to construct a "raw query" to read the ContractInfo 57 | of any CW2-compliant contract. 58 | -------------------------------------------------------------------------------- /packages/cw2/src/lib.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{HumanAddr, Querier, QueryRequest, StdResult, Storage, WasmQuery}; 5 | use cw_storage_plus::Item; 6 | 7 | pub const CONTRACT: Item = Item::new(b"contract_info"); 8 | 9 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 10 | pub struct ContractVersion { 11 | /// contract is the crate name of the implementing contract, eg. `crate:cw20-base` 12 | /// we will use other prefixes for other languages, and their standard global namespacing 13 | pub contract: String, 14 | /// version is any string that this implementation knows. It may be simple counter "1", "2". 15 | /// or semantic version on release tags "v0.6.2", or some custom feature flag list. 16 | /// the only code that needs to understand the version parsing is code that knows how to 17 | /// migrate from the given contract (and is tied to it's implementation somehow) 18 | pub version: String, 19 | } 20 | 21 | /// get_contract_version can be use in migrate to read the previous version of this contract 22 | pub fn get_contract_version(store: &S) -> StdResult { 23 | CONTRACT.load(store) 24 | } 25 | 26 | /// set_contract_version should be used in init to store the original version, and after a successful 27 | /// migrate to update it 28 | pub fn set_contract_version, U: Into>( 29 | store: &mut S, 30 | name: T, 31 | version: U, 32 | ) -> StdResult<()> { 33 | let val = ContractVersion { 34 | contract: name.into(), 35 | version: version.into(), 36 | }; 37 | CONTRACT.save(store, &val) 38 | } 39 | 40 | /// This will make a raw_query to another contract to determine the current version it 41 | /// claims to be. This should not be trusted, but could be used as a quick filter 42 | /// if the other contract exists and claims to be a cw20-base contract for example. 43 | /// (Note: you usually want to require *interfaces* not *implementations* of the 44 | /// contracts you compose with, so be careful of overuse) 45 | pub fn query_contract_info>( 46 | querier: &Q, 47 | contract_addr: T, 48 | ) -> StdResult { 49 | let req = QueryRequest::Wasm(WasmQuery::Raw { 50 | contract_addr: contract_addr.into(), 51 | key: CONTRACT.as_slice().into(), 52 | }); 53 | querier.query(&req) 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | use cosmwasm_std::testing::MockStorage; 60 | 61 | #[test] 62 | fn get_and_set_work() { 63 | let mut store = MockStorage::new(); 64 | 65 | // error if not set 66 | assert!(get_contract_version(&store).is_err()); 67 | 68 | // set and get 69 | let contract_name = "crate:cw20-base"; 70 | let contract_version = "0.2.0"; 71 | set_contract_version(&mut store, contract_name, contract_version).unwrap(); 72 | 73 | let loaded = get_contract_version(&store).unwrap(); 74 | let expected = ContractVersion { 75 | contract: contract_name.to_string(), 76 | version: contract_version.to_string(), 77 | }; 78 | assert_eq!(expected, loaded); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/cw20/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | schema = "run --example schema" 5 | -------------------------------------------------------------------------------- /packages/cw20/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw20" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Definition and types for the CosmWasm-20 interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cw0 = { path = "../../packages/cw0", version = "0.3.2" } 14 | cosmwasm-std = { version = "0.11.1" } 15 | schemars = "0.7" 16 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 17 | 18 | [dev-dependencies] 19 | cosmwasm-schema = { version = "0.11.1" } 20 | -------------------------------------------------------------------------------- /packages/cw20/NOTICE: -------------------------------------------------------------------------------- 1 | CW20: A CosmWasm spec for fungible token contracts 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw20/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | use cw20::{ 7 | AllAccountsResponse, AllAllowancesResponse, AllowanceResponse, BalanceResponse, Cw20HandleMsg, 8 | Cw20QueryMsg, Cw20ReceiveMsg, MinterResponse, TokenInfoResponse, 9 | }; 10 | 11 | fn main() { 12 | let mut out_dir = current_dir().unwrap(); 13 | out_dir.push("schema"); 14 | create_dir_all(&out_dir).unwrap(); 15 | remove_schemas(&out_dir).unwrap(); 16 | 17 | export_schema(&schema_for!(Cw20HandleMsg), &out_dir); 18 | export_schema(&schema_for!(Cw20QueryMsg), &out_dir); 19 | export_schema(&schema_for!(Cw20ReceiveMsg), &out_dir); 20 | export_schema(&schema_for!(AllowanceResponse), &out_dir); 21 | export_schema(&schema_for!(BalanceResponse), &out_dir); 22 | export_schema(&schema_for!(TokenInfoResponse), &out_dir); 23 | export_schema(&schema_for!(MinterResponse), &out_dir); 24 | export_schema(&schema_for!(AllAllowancesResponse), &out_dir); 25 | export_schema(&schema_for!(AllAccountsResponse), &out_dir); 26 | } 27 | -------------------------------------------------------------------------------- /packages/cw20/schema/all_accounts_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AllAccountsResponse", 4 | "type": "object", 5 | "required": [ 6 | "accounts" 7 | ], 8 | "properties": { 9 | "accounts": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/HumanAddr" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "HumanAddr": { 18 | "type": "string" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cw20/schema/all_allowances_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AllAllowancesResponse", 4 | "type": "object", 5 | "required": [ 6 | "allowances" 7 | ], 8 | "properties": { 9 | "allowances": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/AllowanceInfo" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "AllowanceInfo": { 18 | "type": "object", 19 | "required": [ 20 | "allowance", 21 | "expires", 22 | "spender" 23 | ], 24 | "properties": { 25 | "allowance": { 26 | "$ref": "#/definitions/Uint128" 27 | }, 28 | "expires": { 29 | "$ref": "#/definitions/Expiration" 30 | }, 31 | "spender": { 32 | "$ref": "#/definitions/HumanAddr" 33 | } 34 | } 35 | }, 36 | "Expiration": { 37 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 38 | "anyOf": [ 39 | { 40 | "description": "AtHeight will expire when `env.block.height` >= height", 41 | "type": "object", 42 | "required": [ 43 | "at_height" 44 | ], 45 | "properties": { 46 | "at_height": { 47 | "type": "integer", 48 | "format": "uint64", 49 | "minimum": 0.0 50 | } 51 | } 52 | }, 53 | { 54 | "description": "AtTime will expire when `env.block.time` >= time", 55 | "type": "object", 56 | "required": [ 57 | "at_time" 58 | ], 59 | "properties": { 60 | "at_time": { 61 | "type": "integer", 62 | "format": "uint64", 63 | "minimum": 0.0 64 | } 65 | } 66 | }, 67 | { 68 | "description": "Never will never expire. Used to express the empty variant", 69 | "type": "object", 70 | "required": [ 71 | "never" 72 | ], 73 | "properties": { 74 | "never": { 75 | "type": "object" 76 | } 77 | } 78 | } 79 | ] 80 | }, 81 | "HumanAddr": { 82 | "type": "string" 83 | }, 84 | "Uint128": { 85 | "type": "string" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/cw20/schema/allowance_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AllowanceResponse", 4 | "type": "object", 5 | "required": [ 6 | "allowance", 7 | "expires" 8 | ], 9 | "properties": { 10 | "allowance": { 11 | "$ref": "#/definitions/Uint128" 12 | }, 13 | "expires": { 14 | "$ref": "#/definitions/Expiration" 15 | } 16 | }, 17 | "definitions": { 18 | "Expiration": { 19 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 20 | "anyOf": [ 21 | { 22 | "description": "AtHeight will expire when `env.block.height` >= height", 23 | "type": "object", 24 | "required": [ 25 | "at_height" 26 | ], 27 | "properties": { 28 | "at_height": { 29 | "type": "integer", 30 | "format": "uint64", 31 | "minimum": 0.0 32 | } 33 | } 34 | }, 35 | { 36 | "description": "AtTime will expire when `env.block.time` >= time", 37 | "type": "object", 38 | "required": [ 39 | "at_time" 40 | ], 41 | "properties": { 42 | "at_time": { 43 | "type": "integer", 44 | "format": "uint64", 45 | "minimum": 0.0 46 | } 47 | } 48 | }, 49 | { 50 | "description": "Never will never expire. Used to express the empty variant", 51 | "type": "object", 52 | "required": [ 53 | "never" 54 | ], 55 | "properties": { 56 | "never": { 57 | "type": "object" 58 | } 59 | } 60 | } 61 | ] 62 | }, 63 | "Uint128": { 64 | "type": "string" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/cw20/schema/balance_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "BalanceResponse", 4 | "type": "object", 5 | "required": [ 6 | "balance" 7 | ], 8 | "properties": { 9 | "balance": { 10 | "$ref": "#/definitions/Uint128" 11 | } 12 | }, 13 | "definitions": { 14 | "Uint128": { 15 | "type": "string" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cw20/schema/cw20_query_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw20QueryMsg", 4 | "anyOf": [ 5 | { 6 | "description": "Returns the current balance of the given address, 0 if unset. Return type: BalanceResponse.", 7 | "type": "object", 8 | "required": [ 9 | "balance" 10 | ], 11 | "properties": { 12 | "balance": { 13 | "type": "object", 14 | "required": [ 15 | "address" 16 | ], 17 | "properties": { 18 | "address": { 19 | "$ref": "#/definitions/HumanAddr" 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | { 26 | "description": "Returns metadata on the contract - name, decimals, supply, etc. Return type: TokenInfoResponse.", 27 | "type": "object", 28 | "required": [ 29 | "token_info" 30 | ], 31 | "properties": { 32 | "token_info": { 33 | "type": "object" 34 | } 35 | } 36 | }, 37 | { 38 | "description": "Only with \"allowance\" extension. Returns how much spender can use from owner account, 0 if unset. Return type: AllowanceResponse.", 39 | "type": "object", 40 | "required": [ 41 | "allowance" 42 | ], 43 | "properties": { 44 | "allowance": { 45 | "type": "object", 46 | "required": [ 47 | "owner", 48 | "spender" 49 | ], 50 | "properties": { 51 | "owner": { 52 | "$ref": "#/definitions/HumanAddr" 53 | }, 54 | "spender": { 55 | "$ref": "#/definitions/HumanAddr" 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "description": "Only with \"mintable\" extension. Returns who can mint and how much. Return type: MinterResponse.", 63 | "type": "object", 64 | "required": [ 65 | "minter" 66 | ], 67 | "properties": { 68 | "minter": { 69 | "type": "object" 70 | } 71 | } 72 | }, 73 | { 74 | "description": "Only with \"enumerable\" extension (and \"allowances\") Returns all allowances this owner has approved. Supports pagination. Return type: AllAllowancesResponse.", 75 | "type": "object", 76 | "required": [ 77 | "all_allowances" 78 | ], 79 | "properties": { 80 | "all_allowances": { 81 | "type": "object", 82 | "required": [ 83 | "owner" 84 | ], 85 | "properties": { 86 | "limit": { 87 | "type": [ 88 | "integer", 89 | "null" 90 | ], 91 | "format": "uint32", 92 | "minimum": 0.0 93 | }, 94 | "owner": { 95 | "$ref": "#/definitions/HumanAddr" 96 | }, 97 | "start_after": { 98 | "anyOf": [ 99 | { 100 | "$ref": "#/definitions/HumanAddr" 101 | }, 102 | { 103 | "type": "null" 104 | } 105 | ] 106 | } 107 | } 108 | } 109 | } 110 | }, 111 | { 112 | "description": "Only with \"enumerable\" extension Returns all accounts that have balances. Supports pagination. Return type: AllAccountsResponse.", 113 | "type": "object", 114 | "required": [ 115 | "all_accounts" 116 | ], 117 | "properties": { 118 | "all_accounts": { 119 | "type": "object", 120 | "properties": { 121 | "limit": { 122 | "type": [ 123 | "integer", 124 | "null" 125 | ], 126 | "format": "uint32", 127 | "minimum": 0.0 128 | }, 129 | "start_after": { 130 | "anyOf": [ 131 | { 132 | "$ref": "#/definitions/HumanAddr" 133 | }, 134 | { 135 | "type": "null" 136 | } 137 | ] 138 | } 139 | } 140 | } 141 | } 142 | } 143 | ], 144 | "definitions": { 145 | "HumanAddr": { 146 | "type": "string" 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /packages/cw20/schema/cw20_receive_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw20ReceiveMsg", 4 | "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg", 5 | "type": "object", 6 | "required": [ 7 | "amount", 8 | "sender" 9 | ], 10 | "properties": { 11 | "amount": { 12 | "$ref": "#/definitions/Uint128" 13 | }, 14 | "msg": { 15 | "anyOf": [ 16 | { 17 | "$ref": "#/definitions/Binary" 18 | }, 19 | { 20 | "type": "null" 21 | } 22 | ] 23 | }, 24 | "sender": { 25 | "$ref": "#/definitions/HumanAddr" 26 | } 27 | }, 28 | "definitions": { 29 | "Binary": { 30 | "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", 31 | "type": "string" 32 | }, 33 | "HumanAddr": { 34 | "type": "string" 35 | }, 36 | "Uint128": { 37 | "type": "string" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cw20/schema/minter_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "MinterResponse", 4 | "type": "object", 5 | "required": [ 6 | "minter" 7 | ], 8 | "properties": { 9 | "cap": { 10 | "description": "cap is how many more tokens can be issued by the minter", 11 | "anyOf": [ 12 | { 13 | "$ref": "#/definitions/Uint128" 14 | }, 15 | { 16 | "type": "null" 17 | } 18 | ] 19 | }, 20 | "minter": { 21 | "$ref": "#/definitions/HumanAddr" 22 | } 23 | }, 24 | "definitions": { 25 | "HumanAddr": { 26 | "type": "string" 27 | }, 28 | "Uint128": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/cw20/schema/token_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "TokenInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "decimals", 7 | "name", 8 | "symbol", 9 | "total_supply" 10 | ], 11 | "properties": { 12 | "decimals": { 13 | "type": "integer", 14 | "format": "uint8", 15 | "minimum": 0.0 16 | }, 17 | "name": { 18 | "type": "string" 19 | }, 20 | "symbol": { 21 | "type": "string" 22 | }, 23 | "total_supply": { 24 | "$ref": "#/definitions/Uint128" 25 | } 26 | }, 27 | "definitions": { 28 | "Uint128": { 29 | "type": "string" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/cw20/src/balance.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::Coin; 5 | use cw0::NativeBalance; 6 | 7 | use crate::Cw20Coin; 8 | 9 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 10 | #[serde(rename_all = "snake_case")] 11 | pub enum Balance { 12 | Native(NativeBalance), 13 | Cw20(Cw20Coin), 14 | } 15 | 16 | impl Default for Balance { 17 | fn default() -> Balance { 18 | Balance::Native(NativeBalance(vec![])) 19 | } 20 | } 21 | 22 | impl Balance { 23 | pub fn is_empty(&self) -> bool { 24 | match self { 25 | Balance::Native(balance) => balance.is_empty(), 26 | Balance::Cw20(coin) => coin.is_empty(), 27 | } 28 | } 29 | 30 | /// normalize Wallet 31 | pub fn normalize(&mut self) { 32 | match self { 33 | Balance::Native(balance) => balance.normalize(), 34 | Balance::Cw20(_) => {} 35 | } 36 | } 37 | } 38 | 39 | impl From> for Balance { 40 | fn from(coins: Vec) -> Balance { 41 | Balance::Native(NativeBalance(coins)) 42 | } 43 | } 44 | 45 | impl From for Balance { 46 | fn from(cw20_coin: Cw20Coin) -> Balance { 47 | Balance::Cw20(cw20_coin) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cw20/src/coin.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{CanonicalAddr, HumanAddr, Uint128}; 5 | 6 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 7 | pub struct Cw20Coin { 8 | pub address: CanonicalAddr, 9 | pub amount: Uint128, 10 | } 11 | 12 | impl Cw20Coin { 13 | pub fn is_empty(&self) -> bool { 14 | self.amount == Uint128(0) 15 | } 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 19 | pub struct Cw20CoinHuman { 20 | pub address: HumanAddr, 21 | pub amount: Uint128, 22 | } 23 | -------------------------------------------------------------------------------- /packages/cw20/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{ 5 | to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, Querier, StdResult, Uint128, WasmMsg, 6 | WasmQuery, 7 | }; 8 | 9 | use crate::{ 10 | AllowanceResponse, BalanceResponse, Cw20HandleMsg, Cw20QueryMsg, MinterResponse, 11 | TokenInfoResponse, 12 | }; 13 | 14 | /// Cw20Contract is a wrapper around HumanAddr that provides a lot of helpers 15 | /// for working with this. 16 | /// 17 | /// If you wish to persist this, convert to Cw20CanonicalContract via .canonical() 18 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 19 | pub struct Cw20Contract(pub HumanAddr); 20 | 21 | impl Cw20Contract { 22 | pub fn addr(&self) -> HumanAddr { 23 | self.0.clone() 24 | } 25 | 26 | /// Convert this address to a form fit for storage 27 | pub fn canonical(&self, api: &A) -> StdResult { 28 | let canon = api.canonical_address(&self.0)?; 29 | Ok(Cw20CanonicalContract(canon)) 30 | } 31 | 32 | pub fn call>(&self, msg: T) -> StdResult { 33 | let msg = to_binary(&msg.into())?; 34 | Ok(WasmMsg::Execute { 35 | contract_addr: self.addr(), 36 | msg, 37 | send: vec![], 38 | } 39 | .into()) 40 | } 41 | 42 | /// Get token balance for the given address 43 | pub fn balance(&self, querier: &Q, address: HumanAddr) -> StdResult { 44 | let msg = Cw20QueryMsg::Balance { address }; 45 | let query = WasmQuery::Smart { 46 | contract_addr: self.addr(), 47 | msg: to_binary(&msg)?, 48 | } 49 | .into(); 50 | let res: BalanceResponse = querier.query(&query)?; 51 | Ok(res.balance) 52 | } 53 | 54 | /// Get metadata from the contract. This is a good check that the address 55 | /// is a valid Cw20 contract. 56 | pub fn meta(&self, querier: &Q) -> StdResult { 57 | let msg = Cw20QueryMsg::TokenInfo {}; 58 | let query = WasmQuery::Smart { 59 | contract_addr: self.addr(), 60 | msg: to_binary(&msg)?, 61 | } 62 | .into(); 63 | querier.query(&query) 64 | } 65 | 66 | /// Get allowance of spender to use owner's account 67 | pub fn allowance( 68 | &self, 69 | querier: &Q, 70 | owner: HumanAddr, 71 | spender: HumanAddr, 72 | ) -> StdResult { 73 | let msg = Cw20QueryMsg::Allowance { owner, spender }; 74 | let query = WasmQuery::Smart { 75 | contract_addr: self.addr(), 76 | msg: to_binary(&msg)?, 77 | } 78 | .into(); 79 | querier.query(&query) 80 | } 81 | 82 | /// Find info on who can mint, and how much 83 | pub fn minter(&self, querier: &Q) -> StdResult> { 84 | let msg = Cw20QueryMsg::Minter {}; 85 | let query = WasmQuery::Smart { 86 | contract_addr: self.addr(), 87 | msg: to_binary(&msg)?, 88 | } 89 | .into(); 90 | querier.query(&query) 91 | } 92 | 93 | /// returns true if the contract supports the allowance extension 94 | pub fn has_allowance(&self, querier: &Q) -> bool { 95 | self.allowance(querier, self.addr(), self.addr()).is_ok() 96 | } 97 | 98 | /// returns true if the contract supports the mintable extension 99 | pub fn is_mintable(&self, querier: &Q) -> bool { 100 | self.minter(querier).is_ok() 101 | } 102 | } 103 | 104 | /// This is a respresentation of Cw20Contract for storage. 105 | /// Don't use it directly, just translate to the Cw20Contract when needed. 106 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 107 | pub struct Cw20CanonicalContract(pub CanonicalAddr); 108 | 109 | impl Cw20CanonicalContract { 110 | /// Convert this address to a form fit for usage in messages and queries 111 | pub fn human(&self, api: &A) -> StdResult { 112 | let human = api.human_address(&self.0)?; 113 | Ok(Cw20Contract(human)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/cw20/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod balance; 2 | mod coin; 3 | mod helpers; 4 | mod msg; 5 | mod query; 6 | mod receiver; 7 | 8 | pub use cw0::Expiration; 9 | 10 | pub use crate::balance::Balance; 11 | pub use crate::coin::{Cw20Coin, Cw20CoinHuman}; 12 | pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; 13 | pub use crate::msg::Cw20HandleMsg; 14 | pub use crate::query::{ 15 | AllAccountsResponse, AllAllowancesResponse, AllowanceInfo, AllowanceResponse, BalanceResponse, 16 | Cw20QueryMsg, MinterResponse, TokenInfoResponse, 17 | }; 18 | pub use crate::receiver::Cw20ReceiveMsg; 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | #[test] 23 | fn it_works() { 24 | assert_eq!(2 + 2, 4); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cw20/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{Binary, HumanAddr, Uint128}; 5 | use cw0::Expiration; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum Cw20HandleMsg { 10 | /// Transfer is a base message to move tokens to another account without triggering actions 11 | Transfer { 12 | recipient: HumanAddr, 13 | amount: Uint128, 14 | }, 15 | /// Burn is a base message to destroy tokens forever 16 | Burn { amount: Uint128 }, 17 | /// Send is a base message to transfer tokens to a contract and trigger an action 18 | /// on the receiving contract. 19 | Send { 20 | contract: HumanAddr, 21 | amount: Uint128, 22 | msg: Option, 23 | }, 24 | /// Only with "approval" extension. Allows spender to access an additional amount tokens 25 | /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance 26 | /// expiration with this one. 27 | IncreaseAllowance { 28 | spender: HumanAddr, 29 | amount: Uint128, 30 | expires: Option, 31 | }, 32 | /// Only with "approval" extension. Lowers the spender's access of tokens 33 | /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current 34 | /// allowance expiration with this one. 35 | DecreaseAllowance { 36 | spender: HumanAddr, 37 | amount: Uint128, 38 | expires: Option, 39 | }, 40 | /// Only with "approval" extension. Transfers amount tokens from owner -> recipient 41 | /// if `env.sender` has sufficient pre-approval. 42 | TransferFrom { 43 | owner: HumanAddr, 44 | recipient: HumanAddr, 45 | amount: Uint128, 46 | }, 47 | /// Only with "approval" extension. Sends amount tokens from owner -> contract 48 | /// if `env.sender` has sufficient pre-approval. 49 | SendFrom { 50 | owner: HumanAddr, 51 | contract: HumanAddr, 52 | amount: Uint128, 53 | msg: Option, 54 | }, 55 | /// Only with "approval" extension. Destroys tokens forever 56 | BurnFrom { owner: HumanAddr, amount: Uint128 }, 57 | /// Only with the "mintable" extension. If authorized, creates amount new tokens 58 | /// and adds to the recipient balance. 59 | Mint { 60 | recipient: HumanAddr, 61 | amount: Uint128, 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /packages/cw20/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{HumanAddr, Uint128}; 5 | 6 | use cw0::Expiration; 7 | 8 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum Cw20QueryMsg { 11 | /// Returns the current balance of the given address, 0 if unset. 12 | /// Return type: BalanceResponse. 13 | Balance { address: HumanAddr }, 14 | /// Returns metadata on the contract - name, decimals, supply, etc. 15 | /// Return type: TokenInfoResponse. 16 | TokenInfo {}, 17 | /// Only with "allowance" extension. 18 | /// Returns how much spender can use from owner account, 0 if unset. 19 | /// Return type: AllowanceResponse. 20 | Allowance { 21 | owner: HumanAddr, 22 | spender: HumanAddr, 23 | }, 24 | /// Only with "mintable" extension. 25 | /// Returns who can mint and how much. 26 | /// Return type: MinterResponse. 27 | Minter {}, 28 | /// Only with "enumerable" extension (and "allowances") 29 | /// Returns all allowances this owner has approved. Supports pagination. 30 | /// Return type: AllAllowancesResponse. 31 | AllAllowances { 32 | owner: HumanAddr, 33 | start_after: Option, 34 | limit: Option, 35 | }, 36 | /// Only with "enumerable" extension 37 | /// Returns all accounts that have balances. Supports pagination. 38 | /// Return type: AllAccountsResponse. 39 | AllAccounts { 40 | start_after: Option, 41 | limit: Option, 42 | }, 43 | } 44 | 45 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 46 | pub struct BalanceResponse { 47 | pub balance: Uint128, 48 | } 49 | 50 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 51 | pub struct TokenInfoResponse { 52 | pub name: String, 53 | pub symbol: String, 54 | pub decimals: u8, 55 | pub total_supply: Uint128, 56 | } 57 | 58 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 59 | pub struct AllowanceResponse { 60 | pub allowance: Uint128, 61 | pub expires: Expiration, 62 | } 63 | 64 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 65 | pub struct MinterResponse { 66 | pub minter: HumanAddr, 67 | /// cap is how many more tokens can be issued by the minter 68 | pub cap: Option, 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 72 | pub struct AllowanceInfo { 73 | pub spender: HumanAddr, 74 | pub allowance: Uint128, 75 | pub expires: Expiration, 76 | } 77 | 78 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 79 | pub struct AllAllowancesResponse { 80 | pub allowances: Vec, 81 | } 82 | 83 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Default)] 84 | pub struct AllAccountsResponse { 85 | pub accounts: Vec, 86 | } 87 | -------------------------------------------------------------------------------- /packages/cw20/src/receiver.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{to_binary, Binary, CosmosMsg, HumanAddr, StdResult, Uint128, WasmMsg}; 5 | 6 | /// Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct Cw20ReceiveMsg { 10 | pub sender: HumanAddr, 11 | pub amount: Uint128, 12 | pub msg: Option, 13 | } 14 | 15 | impl Cw20ReceiveMsg { 16 | /// serializes the message 17 | pub fn into_binary(self) -> StdResult { 18 | let msg = ReceiverHandleMsg::Receive(self); 19 | to_binary(&msg) 20 | } 21 | 22 | /// creates a cosmos_msg sending this struct to the named contract 23 | pub fn into_cosmos_msg(self, contract_addr: HumanAddr) -> StdResult { 24 | let msg = self.into_binary()?; 25 | let execute = WasmMsg::Execute { 26 | contract_addr, 27 | msg, 28 | send: vec![], 29 | }; 30 | Ok(execute.into()) 31 | } 32 | } 33 | 34 | // This is just a helper to properly serialize the above message 35 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 36 | #[serde(rename_all = "snake_case")] 37 | enum ReceiverHandleMsg { 38 | Receive(Cw20ReceiveMsg), 39 | } 40 | -------------------------------------------------------------------------------- /packages/cw3/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | schema = "run --example schema" 5 | -------------------------------------------------------------------------------- /packages/cw3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw3" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "CosmWasm-3 Interface: On-Chain MultiSig/Voting contracts" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cw0 = { path = "../../packages/cw0", version = "0.3.2" } 14 | cosmwasm-std = { version = "0.11.1" } 15 | schemars = "0.7" 16 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 17 | 18 | [dev-dependencies] 19 | cosmwasm-schema = { version = "0.11.1" } 20 | -------------------------------------------------------------------------------- /packages/cw3/NOTICE: -------------------------------------------------------------------------------- 1 | CW3: A CosmWasm spec for on-chain multiSig/voting contracts 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw3/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; 5 | 6 | use cw3::{ 7 | Cw3HandleMsg, Cw3QueryMsg, ProposalListResponse, ProposalResponse, ThresholdResponse, 8 | VoteListResponse, VoteResponse, VoterListResponse, VoterResponse, 9 | }; 10 | 11 | fn main() { 12 | let mut out_dir = current_dir().unwrap(); 13 | out_dir.push("schema"); 14 | create_dir_all(&out_dir).unwrap(); 15 | remove_schemas(&out_dir).unwrap(); 16 | 17 | export_schema_with_title(&mut schema_for!(Cw3HandleMsg), &out_dir, "HandleMsg"); 18 | export_schema_with_title(&mut schema_for!(Cw3QueryMsg), &out_dir, "QueryMsg"); 19 | export_schema_with_title( 20 | &mut schema_for!(ProposalResponse), 21 | &out_dir, 22 | "ProposalResponse", 23 | ); 24 | export_schema(&schema_for!(ProposalListResponse), &out_dir); 25 | export_schema(&schema_for!(VoteResponse), &out_dir); 26 | export_schema(&schema_for!(VoteListResponse), &out_dir); 27 | export_schema(&schema_for!(VoterResponse), &out_dir); 28 | export_schema(&schema_for!(VoterListResponse), &out_dir); 29 | export_schema(&schema_for!(ThresholdResponse), &out_dir); 30 | } 31 | -------------------------------------------------------------------------------- /packages/cw3/schema/query_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "QueryMsg", 4 | "anyOf": [ 5 | { 6 | "description": "Return ThresholdResponse", 7 | "type": "object", 8 | "required": [ 9 | "threshold" 10 | ], 11 | "properties": { 12 | "threshold": { 13 | "type": "object" 14 | } 15 | } 16 | }, 17 | { 18 | "description": "Returns ProposalResponse", 19 | "type": "object", 20 | "required": [ 21 | "proposal" 22 | ], 23 | "properties": { 24 | "proposal": { 25 | "type": "object", 26 | "required": [ 27 | "proposal_id" 28 | ], 29 | "properties": { 30 | "proposal_id": { 31 | "type": "integer", 32 | "format": "uint64", 33 | "minimum": 0.0 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | { 40 | "description": "Returns ProposalListResponse", 41 | "type": "object", 42 | "required": [ 43 | "list_proposals" 44 | ], 45 | "properties": { 46 | "list_proposals": { 47 | "type": "object", 48 | "properties": { 49 | "limit": { 50 | "type": [ 51 | "integer", 52 | "null" 53 | ], 54 | "format": "uint32", 55 | "minimum": 0.0 56 | }, 57 | "start_after": { 58 | "type": [ 59 | "integer", 60 | "null" 61 | ], 62 | "format": "uint64", 63 | "minimum": 0.0 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | { 70 | "description": "Returns ProposalListResponse", 71 | "type": "object", 72 | "required": [ 73 | "reverse_proposals" 74 | ], 75 | "properties": { 76 | "reverse_proposals": { 77 | "type": "object", 78 | "properties": { 79 | "limit": { 80 | "type": [ 81 | "integer", 82 | "null" 83 | ], 84 | "format": "uint32", 85 | "minimum": 0.0 86 | }, 87 | "start_before": { 88 | "type": [ 89 | "integer", 90 | "null" 91 | ], 92 | "format": "uint64", 93 | "minimum": 0.0 94 | } 95 | } 96 | } 97 | } 98 | }, 99 | { 100 | "description": "Returns VoteResponse", 101 | "type": "object", 102 | "required": [ 103 | "vote" 104 | ], 105 | "properties": { 106 | "vote": { 107 | "type": "object", 108 | "required": [ 109 | "proposal_id", 110 | "voter" 111 | ], 112 | "properties": { 113 | "proposal_id": { 114 | "type": "integer", 115 | "format": "uint64", 116 | "minimum": 0.0 117 | }, 118 | "voter": { 119 | "$ref": "#/definitions/HumanAddr" 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | { 126 | "description": "Returns VoteListResponse", 127 | "type": "object", 128 | "required": [ 129 | "list_votes" 130 | ], 131 | "properties": { 132 | "list_votes": { 133 | "type": "object", 134 | "required": [ 135 | "proposal_id" 136 | ], 137 | "properties": { 138 | "limit": { 139 | "type": [ 140 | "integer", 141 | "null" 142 | ], 143 | "format": "uint32", 144 | "minimum": 0.0 145 | }, 146 | "proposal_id": { 147 | "type": "integer", 148 | "format": "uint64", 149 | "minimum": 0.0 150 | }, 151 | "start_after": { 152 | "anyOf": [ 153 | { 154 | "$ref": "#/definitions/HumanAddr" 155 | }, 156 | { 157 | "type": "null" 158 | } 159 | ] 160 | } 161 | } 162 | } 163 | } 164 | }, 165 | { 166 | "description": "Voter extension: Returns VoterResponse", 167 | "type": "object", 168 | "required": [ 169 | "voter" 170 | ], 171 | "properties": { 172 | "voter": { 173 | "type": "object", 174 | "required": [ 175 | "address" 176 | ], 177 | "properties": { 178 | "address": { 179 | "$ref": "#/definitions/HumanAddr" 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | { 186 | "description": "Voter extension: Returns VoterListResponse", 187 | "type": "object", 188 | "required": [ 189 | "list_voters" 190 | ], 191 | "properties": { 192 | "list_voters": { 193 | "type": "object", 194 | "properties": { 195 | "limit": { 196 | "type": [ 197 | "integer", 198 | "null" 199 | ], 200 | "format": "uint32", 201 | "minimum": 0.0 202 | }, 203 | "start_after": { 204 | "anyOf": [ 205 | { 206 | "$ref": "#/definitions/HumanAddr" 207 | }, 208 | { 209 | "type": "null" 210 | } 211 | ] 212 | } 213 | } 214 | } 215 | } 216 | } 217 | ], 218 | "definitions": { 219 | "HumanAddr": { 220 | "type": "string" 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /packages/cw3/schema/threshold_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ThresholdResponse", 4 | "description": "This defines the different ways tallies can happen. It can be extended as needed, but once the spec is frozen, these should not be modified. They are designed to be general.", 5 | "anyOf": [ 6 | { 7 | "description": "Declares a total weight needed to pass This usually implies that count_needed is stable, even if total_weight changes eg. 3 of 5 multisig -> 3 of 6 multisig", 8 | "type": "object", 9 | "required": [ 10 | "absolute_count" 11 | ], 12 | "properties": { 13 | "absolute_count": { 14 | "type": "object", 15 | "required": [ 16 | "total_weight", 17 | "weight_needed" 18 | ], 19 | "properties": { 20 | "total_weight": { 21 | "type": "integer", 22 | "format": "uint64", 23 | "minimum": 0.0 24 | }, 25 | "weight_needed": { 26 | "type": "integer", 27 | "format": "uint64", 28 | "minimum": 0.0 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | { 35 | "description": "Declares a percentage of the total weight needed to pass This implies the percentage is stable, when total_weight changes eg. at 50.1%, we go from needing 51/100 to needing 101/200\n\nNote: percentage_needed = 60% is different than threshold = 60%, quora = 100% as the first will pass with 60% yes votes and 10% no votes, while the second will require the others to vote anything (no, abstain...) to pass", 36 | "type": "object", 37 | "required": [ 38 | "absolute_percentage" 39 | ], 40 | "properties": { 41 | "absolute_percentage": { 42 | "type": "object", 43 | "required": [ 44 | "percentage_needed", 45 | "total_weight" 46 | ], 47 | "properties": { 48 | "percentage_needed": { 49 | "$ref": "#/definitions/Decimal" 50 | }, 51 | "total_weight": { 52 | "type": "integer", 53 | "format": "uint64", 54 | "minimum": 0.0 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | { 61 | "description": "Declares a threshold (minimum percentage of votes that must approve) and a quorum (minimum percentage of voter weight that must vote). This allows eg. 25% of total weight YES to pass, if we have quorum of 40% and threshold of 51% and most of the people sit out the election. This is more common in general elections where participation is expected to be low.", 62 | "type": "object", 63 | "required": [ 64 | "threshold_quora" 65 | ], 66 | "properties": { 67 | "threshold_quora": { 68 | "type": "object", 69 | "required": [ 70 | "quroum", 71 | "threshold", 72 | "total_weight" 73 | ], 74 | "properties": { 75 | "quroum": { 76 | "$ref": "#/definitions/Decimal" 77 | }, 78 | "threshold": { 79 | "$ref": "#/definitions/Decimal" 80 | }, 81 | "total_weight": { 82 | "type": "integer", 83 | "format": "uint64", 84 | "minimum": 0.0 85 | } 86 | } 87 | } 88 | } 89 | } 90 | ], 91 | "definitions": { 92 | "Decimal": { 93 | "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)", 94 | "type": "string" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/cw3/schema/vote_list_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "VoteListResponse", 4 | "type": "object", 5 | "required": [ 6 | "votes" 7 | ], 8 | "properties": { 9 | "votes": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/VoteInfo" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "HumanAddr": { 18 | "type": "string" 19 | }, 20 | "Vote": { 21 | "type": "string", 22 | "enum": [ 23 | "yes", 24 | "no", 25 | "abstain", 26 | "veto" 27 | ] 28 | }, 29 | "VoteInfo": { 30 | "type": "object", 31 | "required": [ 32 | "vote", 33 | "voter", 34 | "weight" 35 | ], 36 | "properties": { 37 | "vote": { 38 | "$ref": "#/definitions/Vote" 39 | }, 40 | "voter": { 41 | "$ref": "#/definitions/HumanAddr" 42 | }, 43 | "weight": { 44 | "type": "integer", 45 | "format": "uint64", 46 | "minimum": 0.0 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/cw3/schema/vote_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "VoteResponse", 4 | "type": "object", 5 | "properties": { 6 | "vote": { 7 | "anyOf": [ 8 | { 9 | "$ref": "#/definitions/Vote" 10 | }, 11 | { 12 | "type": "null" 13 | } 14 | ] 15 | } 16 | }, 17 | "definitions": { 18 | "Vote": { 19 | "type": "string", 20 | "enum": [ 21 | "yes", 22 | "no", 23 | "abstain", 24 | "veto" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cw3/schema/voter_list_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "VoterListResponse", 4 | "type": "object", 5 | "required": [ 6 | "voters" 7 | ], 8 | "properties": { 9 | "voters": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/VoterResponse" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "HumanAddr": { 18 | "type": "string" 19 | }, 20 | "VoterResponse": { 21 | "type": "object", 22 | "required": [ 23 | "addr", 24 | "weight" 25 | ], 26 | "properties": { 27 | "addr": { 28 | "$ref": "#/definitions/HumanAddr" 29 | }, 30 | "weight": { 31 | "type": "integer", 32 | "format": "uint64", 33 | "minimum": 0.0 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cw3/schema/voter_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "VoterResponse", 4 | "type": "object", 5 | "required": [ 6 | "addr", 7 | "weight" 8 | ], 9 | "properties": { 10 | "addr": { 11 | "$ref": "#/definitions/HumanAddr" 12 | }, 13 | "weight": { 14 | "type": "integer", 15 | "format": "uint64", 16 | "minimum": 0.0 17 | } 18 | }, 19 | "definitions": { 20 | "HumanAddr": { 21 | "type": "string" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cw3/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, StdResult, WasmMsg}; 5 | 6 | use crate::msg::{Cw3HandleMsg, Vote}; 7 | use cw0::Expiration; 8 | 9 | /// Cw3Contract is a wrapper around HumanAddr that provides a lot of helpers 10 | /// for working with this. 11 | /// 12 | /// If you wish to persist this, convert to Cw3CanonicalContract via .canonical() 13 | /// 14 | /// FIXME: Cw3Contract currently only supports CosmosMsg. When we actually 15 | /// use this in some consuming code, we should make it generic over CosmosMsg. 16 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 17 | pub struct Cw3Contract(pub HumanAddr); 18 | 19 | impl Cw3Contract { 20 | pub fn addr(&self) -> HumanAddr { 21 | self.0.clone() 22 | } 23 | 24 | /// Convert this address to a form fit for storage 25 | pub fn canonical(&self, api: &A) -> StdResult { 26 | let canon = api.canonical_address(&self.0)?; 27 | Ok(Cw3CanonicalContract(canon)) 28 | } 29 | 30 | pub fn encode_msg(&self, msg: Cw3HandleMsg) -> StdResult { 31 | Ok(WasmMsg::Execute { 32 | contract_addr: self.addr(), 33 | msg: to_binary(&msg)?, 34 | send: vec![], 35 | } 36 | .into()) 37 | } 38 | 39 | /// helper doesn't support custom messages now 40 | pub fn proposal, U: Into>( 41 | &self, 42 | title: T, 43 | description: U, 44 | msgs: Vec, 45 | earliest: Option, 46 | latest: Option, 47 | ) -> StdResult { 48 | let msg = Cw3HandleMsg::Propose { 49 | title: title.into(), 50 | description: description.into(), 51 | msgs, 52 | earliest, 53 | latest, 54 | }; 55 | self.encode_msg(msg) 56 | } 57 | 58 | pub fn vote(&self, proposal_id: u64, vote: Vote) -> StdResult { 59 | let msg = Cw3HandleMsg::Vote { proposal_id, vote }; 60 | self.encode_msg(msg) 61 | } 62 | 63 | pub fn execute(&self, proposal_id: u64) -> StdResult { 64 | let msg = Cw3HandleMsg::Execute { proposal_id }; 65 | self.encode_msg(msg) 66 | } 67 | 68 | pub fn close(&self, proposal_id: u64) -> StdResult { 69 | let msg = Cw3HandleMsg::Close { proposal_id }; 70 | self.encode_msg(msg) 71 | } 72 | } 73 | 74 | /// This is a respresentation of Cw3Contract for storage. 75 | /// Don't use it directly, just translate to the Cw3Contract when needed. 76 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 77 | pub struct Cw3CanonicalContract(pub CanonicalAddr); 78 | 79 | impl Cw3CanonicalContract { 80 | /// Convert this address to a form fit for usage in messages and queries 81 | pub fn human(&self, api: &A) -> StdResult { 82 | let human = api.human_address(&self.0)?; 83 | Ok(Cw3Contract(human)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/cw3/src/lib.rs: -------------------------------------------------------------------------------- 1 | // mod helpers; 2 | mod helpers; 3 | mod msg; 4 | mod query; 5 | 6 | pub use crate::helpers::{Cw3CanonicalContract, Cw3Contract}; 7 | pub use crate::msg::{Cw3HandleMsg, Vote}; 8 | pub use crate::query::{ 9 | Cw3QueryMsg, ProposalListResponse, ProposalResponse, Status, ThresholdResponse, VoteInfo, 10 | VoteListResponse, VoteResponse, VoterListResponse, VoterResponse, 11 | }; 12 | 13 | #[cfg(test)] 14 | mod tests { 15 | #[test] 16 | fn it_works() { 17 | assert_eq!(2 + 2, 4); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/cw3/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_std::{CosmosMsg, Empty}; 6 | use cw0::Expiration; 7 | 8 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum Cw3HandleMsg 11 | where 12 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 13 | { 14 | Propose { 15 | title: String, 16 | description: String, 17 | msgs: Vec>, 18 | earliest: Option, 19 | latest: Option, 20 | }, 21 | Vote { 22 | proposal_id: u64, 23 | vote: Vote, 24 | }, 25 | Execute { 26 | proposal_id: u64, 27 | }, 28 | Close { 29 | proposal_id: u64, 30 | }, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)] 34 | #[serde(rename_all = "lowercase")] 35 | pub enum Vote { 36 | Yes, 37 | No, 38 | Abstain, 39 | Veto, 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use super::*; 45 | use cosmwasm_std::to_vec; 46 | 47 | #[test] 48 | fn vote_encoding() { 49 | let a = Vote::Yes; 50 | let encoded = to_vec(&a).unwrap(); 51 | let json = String::from_utf8_lossy(&encoded).to_string(); 52 | assert_eq!(r#""yes""#, json.as_str()); 53 | } 54 | 55 | #[test] 56 | fn vote_encoding_embedded() { 57 | let msg = Cw3HandleMsg::Vote:: { 58 | proposal_id: 17, 59 | vote: Vote::No, 60 | }; 61 | let encoded = to_vec(&msg).unwrap(); 62 | let json = String::from_utf8_lossy(&encoded).to_string(); 63 | assert_eq!(r#"{"vote":{"proposal_id":17,"vote":"no"}}"#, json.as_str()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/cw3/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | use std::fmt; 4 | 5 | use cosmwasm_std::{CosmosMsg, Decimal, Empty, HumanAddr}; 6 | use cw0::Expiration; 7 | 8 | use crate::msg::Vote; 9 | 10 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 11 | #[serde(rename_all = "snake_case")] 12 | pub enum Cw3QueryMsg { 13 | /// Return ThresholdResponse 14 | Threshold {}, 15 | /// Returns ProposalResponse 16 | Proposal { proposal_id: u64 }, 17 | /// Returns ProposalListResponse 18 | ListProposals { 19 | start_after: Option, 20 | limit: Option, 21 | }, 22 | /// Returns ProposalListResponse 23 | ReverseProposals { 24 | start_before: Option, 25 | limit: Option, 26 | }, 27 | /// Returns VoteResponse 28 | Vote { proposal_id: u64, voter: HumanAddr }, 29 | /// Returns VoteListResponse 30 | ListVotes { 31 | proposal_id: u64, 32 | start_after: Option, 33 | limit: Option, 34 | }, 35 | /// Voter extension: Returns VoterResponse 36 | Voter { address: HumanAddr }, 37 | /// Voter extension: Returns VoterListResponse 38 | ListVoters { 39 | start_after: Option, 40 | limit: Option, 41 | }, 42 | } 43 | 44 | /// This defines the different ways tallies can happen. 45 | /// It can be extended as needed, but once the spec is frozen, 46 | /// these should not be modified. They are designed to be general. 47 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 48 | #[serde(rename_all = "snake_case")] 49 | pub enum ThresholdResponse { 50 | /// Declares a total weight needed to pass 51 | /// This usually implies that count_needed is stable, even if total_weight changes 52 | /// eg. 3 of 5 multisig -> 3 of 6 multisig 53 | AbsoluteCount { 54 | weight_needed: u64, 55 | total_weight: u64, 56 | }, 57 | /// Declares a percentage of the total weight needed to pass 58 | /// This implies the percentage is stable, when total_weight changes 59 | /// eg. at 50.1%, we go from needing 51/100 to needing 101/200 60 | /// 61 | /// Note: percentage_needed = 60% is different than threshold = 60%, quora = 100% 62 | /// as the first will pass with 60% yes votes and 10% no votes, while the second 63 | /// will require the others to vote anything (no, abstain...) to pass 64 | AbsolutePercentage { 65 | percentage_needed: Decimal, 66 | total_weight: u64, 67 | }, 68 | /// Declares a threshold (minimum percentage of votes that must approve) 69 | /// and a quorum (minimum percentage of voter weight that must vote). 70 | /// This allows eg. 25% of total weight YES to pass, if we have quorum of 40% 71 | /// and threshold of 51% and most of the people sit out the election. 72 | /// This is more common in general elections where participation is expected 73 | /// to be low. 74 | ThresholdQuora { 75 | threshold: Decimal, 76 | quroum: Decimal, 77 | total_weight: u64, 78 | }, 79 | } 80 | 81 | /// Note, if you are storing custom messages in the proposal, 82 | /// the querier needs to know what possible custom message types 83 | /// those are in order to parse the response 84 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 85 | pub struct ProposalResponse 86 | where 87 | T: Clone + fmt::Debug + PartialEq + JsonSchema, 88 | { 89 | pub id: u64, 90 | pub title: String, 91 | pub description: String, 92 | pub msgs: Vec>, 93 | pub expires: Expiration, 94 | pub status: Status, 95 | } 96 | 97 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)] 98 | #[serde(rename_all = "lowercase")] 99 | pub enum Status { 100 | /// proposal was created, but voting has not yet begun for whatever reason 101 | Pending, 102 | /// you can vote on this 103 | Open, 104 | /// voting is over and it did not pass 105 | Rejected, 106 | /// voting is over and it did pass, but has not yet executed 107 | Passed, 108 | /// voting is over it passed, and the proposal was executed 109 | Executed, 110 | } 111 | 112 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 113 | pub struct ProposalListResponse { 114 | pub proposals: Vec, 115 | } 116 | 117 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 118 | pub struct VoteListResponse { 119 | pub votes: Vec, 120 | } 121 | 122 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 123 | pub struct VoteInfo { 124 | pub voter: HumanAddr, 125 | pub vote: Vote, 126 | pub weight: u64, 127 | } 128 | 129 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 130 | pub struct VoteResponse { 131 | pub vote: Option, 132 | } 133 | 134 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 135 | pub struct VoterResponse { 136 | pub addr: HumanAddr, 137 | pub weight: u64, 138 | } 139 | 140 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 141 | pub struct VoterListResponse { 142 | pub voters: Vec, 143 | } 144 | -------------------------------------------------------------------------------- /packages/cw4/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | schema = "run --example schema" 5 | -------------------------------------------------------------------------------- /packages/cw4/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw4" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "CosmWasm-4 Interface: Groups Members" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cosmwasm-std = { version = "0.11.1" } 14 | schemars = "0.7" 15 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 16 | 17 | [dev-dependencies] 18 | cosmwasm-schema = { version = "0.11.1" } 19 | -------------------------------------------------------------------------------- /packages/cw4/NOTICE: -------------------------------------------------------------------------------- 1 | CW3: A CosmWasm spec for on-chain multiSig/voting contracts 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw4/README.md: -------------------------------------------------------------------------------- 1 | # CW4 Spec: Group Members 2 | 3 | CW4 is a spec for storing group membership, which can be combined 4 | with CW3 multisigs. The purpose is to store a set of actors/voters 5 | that can be accessed to determine permissions in another section. 6 | 7 | Since this is often deployed as a contract pair, we expect this 8 | contract to often be queried with `QueryRaw` and the internal 9 | layout of some of the data structures becomes part of the public API. 10 | Implementations may add more data structures, but at least 11 | the ones laid out here should be under the specified keys and in the 12 | same format. 13 | 14 | In this case, a cw3 contract could *read* an external group contract with 15 | no significant cost more than reading local storage. However, updating 16 | that group contract (if allowed), would be an external message and 17 | charged the instantiation overhead for each contract. 18 | 19 | ## Messages 20 | 21 | We define an `InitMsg{admin, members}` to make it easy to set up a group 22 | as part of another flow. Implementations should work with this setup, 23 | but may add extra `Option` fields for non-essential extensions to 24 | configure in the `init` phase. 25 | 26 | There are two messages supported by a group contract: 27 | 28 | `UpdateAdmin{admin}` - changes (or clears) the admin for the contract 29 | 30 | `UpdateMembers{add, remove}` - takes a membership diff and adds/updates the 31 | members, as well as removing any provided addresses. If an address is on both 32 | lists, it will be removed. If it appears multiple times in `add`, only the 33 | last occurance will be used. 34 | 35 | Only the `admin` may execute either of these function. Thus, by omitting an 36 | `admin`, we end up with a similar functionality ad `cw3-fixed-multisig`. 37 | If we include one, it may often be desired to be a `cw3` contract that 38 | uses this group contract as a group. This leads to a bit of chicken-and-egg 39 | problem, but we will cover how to instantiate that in `cw3-flexible-multisig` 40 | when the contract is built (TODO). 41 | 42 | ## Queries 43 | 44 | ### Smart 45 | 46 | `Admin{}` - Returns the `admin` address, or `None` if unset. 47 | 48 | `TotalWeight{}` - Returns the total weight of all current members, 49 | this is very useful if some conditions are defined on a "percentage of members". 50 | 51 | `Member{addr}` - Returns the weight of this voter if they are a member of the 52 | group (may be 0), or `None` if they are not a member of the group. 53 | 54 | `MemberList{start_after, limit}` - Allows us to paginate over the list 55 | of all members. 0-weight members will be included. Removed members will not. 56 | 57 | ### Raw 58 | 59 | In addition to the above "SmartQueries", which make up the public API, 60 | we define two raw queries that are designed for more efficiency 61 | in contract-contract calls. These use keys exported by `cw4` 62 | 63 | `TOTAL_KEY` - making a raw query with this key (`b"total"`) will return a 64 | JSON-encoded `u64` 65 | 66 | `members_key()` - takes a `CanonicalAddr` and returns a key that can be 67 | used for raw query (`"\x00\x07members" || addr`). This will return 68 | empty bytes if the member is not inside the group, otherwise a 69 | JSON-encoded `u64` -------------------------------------------------------------------------------- /packages/cw4/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | pub use cw4::{ 7 | AdminResponse, Cw4HandleMsg, Cw4InitMsg, Cw4QueryMsg, Member, MemberListResponse, 8 | MemberResponse, TotalWeightResponse, 9 | }; 10 | 11 | fn main() { 12 | let mut out_dir = current_dir().unwrap(); 13 | out_dir.push("schema"); 14 | create_dir_all(&out_dir).unwrap(); 15 | remove_schemas(&out_dir).unwrap(); 16 | 17 | export_schema(&schema_for!(Cw4HandleMsg), &out_dir); 18 | export_schema(&schema_for!(Cw4InitMsg), &out_dir); 19 | export_schema(&schema_for!(Cw4QueryMsg), &out_dir); 20 | export_schema(&schema_for!(AdminResponse), &out_dir); 21 | export_schema(&schema_for!(MemberListResponse), &out_dir); 22 | export_schema(&schema_for!(MemberResponse), &out_dir); 23 | export_schema(&schema_for!(TotalWeightResponse), &out_dir); 24 | } 25 | -------------------------------------------------------------------------------- /packages/cw4/schema/admin_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AdminResponse", 4 | "type": "object", 5 | "properties": { 6 | "admin": { 7 | "anyOf": [ 8 | { 9 | "$ref": "#/definitions/HumanAddr" 10 | }, 11 | { 12 | "type": "null" 13 | } 14 | ] 15 | } 16 | }, 17 | "definitions": { 18 | "HumanAddr": { 19 | "type": "string" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cw4/schema/cw4_handle_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw4HandleMsg", 4 | "anyOf": [ 5 | { 6 | "description": "Change the admin", 7 | "type": "object", 8 | "required": [ 9 | "update_admin" 10 | ], 11 | "properties": { 12 | "update_admin": { 13 | "type": "object", 14 | "properties": { 15 | "admin": { 16 | "anyOf": [ 17 | { 18 | "$ref": "#/definitions/HumanAddr" 19 | }, 20 | { 21 | "type": "null" 22 | } 23 | ] 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | { 30 | "description": "apply a diff to the existing members. remove is applied after add, so if an address is in both, it is removed", 31 | "type": "object", 32 | "required": [ 33 | "update_members" 34 | ], 35 | "properties": { 36 | "update_members": { 37 | "type": "object", 38 | "required": [ 39 | "add", 40 | "remove" 41 | ], 42 | "properties": { 43 | "add": { 44 | "type": "array", 45 | "items": { 46 | "$ref": "#/definitions/Member" 47 | } 48 | }, 49 | "remove": { 50 | "type": "array", 51 | "items": { 52 | "$ref": "#/definitions/HumanAddr" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | ], 60 | "definitions": { 61 | "HumanAddr": { 62 | "type": "string" 63 | }, 64 | "Member": { 65 | "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", 66 | "type": "object", 67 | "required": [ 68 | "addr", 69 | "weight" 70 | ], 71 | "properties": { 72 | "addr": { 73 | "$ref": "#/definitions/HumanAddr" 74 | }, 75 | "weight": { 76 | "type": "integer", 77 | "format": "uint64", 78 | "minimum": 0.0 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/cw4/schema/cw4_init_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw4InitMsg", 4 | "type": "object", 5 | "required": [ 6 | "members" 7 | ], 8 | "properties": { 9 | "admin": { 10 | "description": "The admin is the only account that can update the group state. Omit it to make the group immutable.", 11 | "anyOf": [ 12 | { 13 | "$ref": "#/definitions/HumanAddr" 14 | }, 15 | { 16 | "type": "null" 17 | } 18 | ] 19 | }, 20 | "members": { 21 | "type": "array", 22 | "items": { 23 | "$ref": "#/definitions/Member" 24 | } 25 | } 26 | }, 27 | "definitions": { 28 | "HumanAddr": { 29 | "type": "string" 30 | }, 31 | "Member": { 32 | "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", 33 | "type": "object", 34 | "required": [ 35 | "addr", 36 | "weight" 37 | ], 38 | "properties": { 39 | "addr": { 40 | "$ref": "#/definitions/HumanAddr" 41 | }, 42 | "weight": { 43 | "type": "integer", 44 | "format": "uint64", 45 | "minimum": 0.0 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/cw4/schema/cw4_query_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw4QueryMsg", 4 | "anyOf": [ 5 | { 6 | "description": "Return AdminResponse", 7 | "type": "object", 8 | "required": [ 9 | "admin" 10 | ], 11 | "properties": { 12 | "admin": { 13 | "type": "object" 14 | } 15 | } 16 | }, 17 | { 18 | "description": "Return TotalWeightResponse", 19 | "type": "object", 20 | "required": [ 21 | "total_weight" 22 | ], 23 | "properties": { 24 | "total_weight": { 25 | "type": "object" 26 | } 27 | } 28 | }, 29 | { 30 | "description": "Returns MembersListResponse", 31 | "type": "object", 32 | "required": [ 33 | "list_members" 34 | ], 35 | "properties": { 36 | "list_members": { 37 | "type": "object", 38 | "properties": { 39 | "limit": { 40 | "type": [ 41 | "integer", 42 | "null" 43 | ], 44 | "format": "uint32", 45 | "minimum": 0.0 46 | }, 47 | "start_after": { 48 | "anyOf": [ 49 | { 50 | "$ref": "#/definitions/HumanAddr" 51 | }, 52 | { 53 | "type": "null" 54 | } 55 | ] 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "description": "Returns MemberResponse", 63 | "type": "object", 64 | "required": [ 65 | "member" 66 | ], 67 | "properties": { 68 | "member": { 69 | "type": "object", 70 | "required": [ 71 | "addr" 72 | ], 73 | "properties": { 74 | "addr": { 75 | "$ref": "#/definitions/HumanAddr" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | ], 82 | "definitions": { 83 | "HumanAddr": { 84 | "type": "string" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/cw4/schema/member_list_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "MemberListResponse", 4 | "type": "object", 5 | "required": [ 6 | "members" 7 | ], 8 | "properties": { 9 | "members": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/Member" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "HumanAddr": { 18 | "type": "string" 19 | }, 20 | "Member": { 21 | "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", 22 | "type": "object", 23 | "required": [ 24 | "addr", 25 | "weight" 26 | ], 27 | "properties": { 28 | "addr": { 29 | "$ref": "#/definitions/HumanAddr" 30 | }, 31 | "weight": { 32 | "type": "integer", 33 | "format": "uint64", 34 | "minimum": 0.0 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/cw4/schema/member_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "MemberResponse", 4 | "type": "object", 5 | "properties": { 6 | "weight": { 7 | "type": [ 8 | "integer", 9 | "null" 10 | ], 11 | "format": "uint64", 12 | "minimum": 0.0 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cw4/schema/total_weight_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "TotalWeightResponse", 4 | "type": "object", 5 | "required": [ 6 | "weight" 7 | ], 8 | "properties": { 9 | "weight": { 10 | "type": "integer", 11 | "format": "uint64", 12 | "minimum": 0.0 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cw4/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{ 5 | from_slice, to_binary, to_vec, Api, Binary, CanonicalAddr, ContractResult, CosmosMsg, Empty, 6 | HumanAddr, Querier, QueryRequest, StdError, StdResult, SystemResult, WasmMsg, WasmQuery, 7 | }; 8 | 9 | use crate::msg::Cw4HandleMsg; 10 | use crate::{member_key, AdminResponse, Cw4QueryMsg, Member, MemberListResponse, TOTAL_KEY}; 11 | 12 | /// Cw4Contract is a wrapper around HumanAddr that provides a lot of helpers 13 | /// for working with cw4 contracts 14 | /// 15 | /// If you wish to persist this, convert to Cw4CanonicalContract via .canonical() 16 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 17 | pub struct Cw4Contract(pub HumanAddr); 18 | 19 | impl Cw4Contract { 20 | pub fn addr(&self) -> HumanAddr { 21 | self.0.clone() 22 | } 23 | 24 | /// Convert this address to a form fit for storage 25 | pub fn canonical(&self, api: &A) -> StdResult { 26 | let canon = api.canonical_address(&self.0)?; 27 | Ok(Cw4CanonicalContract(canon)) 28 | } 29 | 30 | fn encode_msg(&self, msg: Cw4HandleMsg) -> StdResult { 31 | Ok(WasmMsg::Execute { 32 | contract_addr: self.addr(), 33 | msg: to_binary(&msg)?, 34 | send: vec![], 35 | } 36 | .into()) 37 | } 38 | 39 | pub fn update_admin>( 40 | &self, 41 | admin: Option, 42 | ) -> StdResult { 43 | let msg = Cw4HandleMsg::UpdateAdmin { admin }; 44 | self.encode_msg(msg) 45 | } 46 | 47 | pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { 48 | let msg = Cw4HandleMsg::UpdateMembers { remove, add }; 49 | self.encode_msg(msg) 50 | } 51 | 52 | fn encode_smart_query(&self, msg: Cw4QueryMsg) -> StdResult> { 53 | Ok(WasmQuery::Smart { 54 | contract_addr: self.addr(), 55 | msg: to_binary(&msg)?, 56 | } 57 | .into()) 58 | } 59 | 60 | fn encode_raw_query>(&self, key: T) -> StdResult> { 61 | Ok(WasmQuery::Raw { 62 | contract_addr: self.addr(), 63 | key: key.into(), 64 | } 65 | .into()) 66 | } 67 | 68 | /// Read the admin 69 | pub fn admin(&self, querier: &Q) -> StdResult> { 70 | let query = self.encode_smart_query(Cw4QueryMsg::Admin {})?; 71 | let res: AdminResponse = querier.query(&query)?; 72 | Ok(res.admin) 73 | } 74 | 75 | /// Read the total weight 76 | pub fn total_weight(&self, querier: &Q) -> StdResult { 77 | let query = self.encode_raw_query(TOTAL_KEY)?; 78 | querier.query(&query) 79 | } 80 | 81 | // TODO: implement with raw queries 82 | /// Check if this address is a member, and if so, with which weight 83 | pub fn is_member>( 84 | &self, 85 | querier: &Q, 86 | addr: T, 87 | ) -> StdResult> { 88 | let path = member_key(&addr.into()); 89 | let query = self.encode_raw_query(path)?; 90 | 91 | // We have to copy the logic of Querier.query to handle the empty case, and not 92 | // try to decode empty result into a u64. 93 | // TODO: add similar API on Querier - this is not the first time I came across it 94 | let raw = to_vec(&query)?; 95 | match querier.raw_query(&raw) { 96 | SystemResult::Err(system_err) => Err(StdError::generic_err(format!( 97 | "Querier system error: {}", 98 | system_err 99 | ))), 100 | SystemResult::Ok(ContractResult::Err(contract_err)) => Err(StdError::generic_err( 101 | format!("Querier contract error: {}", contract_err), 102 | )), 103 | SystemResult::Ok(ContractResult::Ok(value)) => { 104 | // This is the only place we customize 105 | if value.is_empty() { 106 | Ok(None) 107 | } else { 108 | from_slice(&value) 109 | } 110 | } 111 | } 112 | } 113 | 114 | pub fn list_members( 115 | &self, 116 | querier: &Q, 117 | start_after: Option, 118 | limit: Option, 119 | ) -> StdResult> { 120 | let query = self.encode_smart_query(Cw4QueryMsg::ListMembers { start_after, limit })?; 121 | let res: MemberListResponse = querier.query(&query)?; 122 | Ok(res.members) 123 | } 124 | } 125 | 126 | /// This is a respresentation of Cw4Contract for storage. 127 | /// Don't use it directly, just translate to the Cw4Contract when needed. 128 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 129 | pub struct Cw4CanonicalContract(pub CanonicalAddr); 130 | 131 | impl Cw4CanonicalContract { 132 | /// Convert this address to a form fit for usage in messages and queries 133 | pub fn human(&self, api: &A) -> StdResult { 134 | let human = api.human_address(&self.0)?; 135 | Ok(Cw4Contract(human)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/cw4/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | mod msg; 3 | mod query; 4 | 5 | pub use crate::helpers::{Cw4CanonicalContract, Cw4Contract}; 6 | pub use crate::msg::{Cw4HandleMsg, Cw4InitMsg, Member}; 7 | pub use crate::query::{ 8 | member_key, AdminResponse, Cw4QueryMsg, MemberListResponse, MemberResponse, 9 | TotalWeightResponse, MEMBERS_KEY, TOTAL_KEY, 10 | }; 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[test] 15 | fn it_works() { 16 | assert_eq!(2 + 2, 4); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/cw4/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::HumanAddr; 5 | 6 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 7 | #[serde(rename_all = "snake_case")] 8 | pub struct Cw4InitMsg { 9 | /// The admin is the only account that can update the group state. 10 | /// Omit it to make the group immutable. 11 | pub admin: Option, 12 | pub members: Vec, 13 | } 14 | 15 | /// A group member has a weight associated with them. 16 | /// This may all be equal, or may have meaning in the app that 17 | /// makes use of the group (eg. voting power) 18 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 19 | pub struct Member { 20 | pub addr: HumanAddr, 21 | pub weight: u64, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 25 | #[serde(rename_all = "snake_case")] 26 | pub enum Cw4HandleMsg { 27 | /// Change the admin 28 | UpdateAdmin { admin: Option }, 29 | /// apply a diff to the existing members. 30 | /// remove is applied after add, so if an address is in both, it is removed 31 | UpdateMembers { 32 | remove: Vec, 33 | add: Vec, 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /packages/cw4/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::HumanAddr; 5 | 6 | use crate::msg::Member; 7 | 8 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum Cw4QueryMsg { 11 | /// Return AdminResponse 12 | Admin {}, 13 | // TODO: this also needs raw query access 14 | /// Return TotalWeightResponse 15 | TotalWeight {}, 16 | /// Returns MembersListResponse 17 | ListMembers { 18 | start_after: Option, 19 | limit: Option, 20 | }, 21 | // TODO: this also needs raw query access 22 | /// Returns MemberResponse 23 | Member { addr: HumanAddr }, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 27 | pub struct MemberListResponse { 28 | pub members: Vec, 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 32 | pub struct MemberResponse { 33 | pub weight: Option, 34 | } 35 | 36 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 37 | pub struct AdminResponse { 38 | pub admin: Option, 39 | } 40 | 41 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 42 | pub struct TotalWeightResponse { 43 | pub weight: u64, 44 | } 45 | 46 | /// TOTAL_KEY is meant for raw queries 47 | pub const TOTAL_KEY: &[u8] = b"total"; 48 | pub const MEMBERS_KEY: &[u8] = b"members"; 49 | 50 | /// member_key is meant for raw queries for one member, given canonical address 51 | pub fn member_key(address: &[u8]) -> Vec { 52 | // length encoded members key (update if you change MEMBERS_KEY) 53 | // inlined here to avoid storage-plus import 54 | let mut key = b"\x00\x07members".to_vec(); 55 | key.extend_from_slice(address); 56 | key 57 | } 58 | -------------------------------------------------------------------------------- /packages/cw721/.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | wasm = "build --release --target wasm32-unknown-unknown" 3 | wasm-debug = "build --target wasm32-unknown-unknown" 4 | schema = "run --example schema" 5 | -------------------------------------------------------------------------------- /packages/cw721/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw721" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Definition and types for the CosmWasm-721 NFT interface" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [dependencies] 13 | cw0 = { path = "../../packages/cw0", version = "0.3.2" } 14 | cosmwasm-std = { version = "0.11.1" } 15 | schemars = "0.7" 16 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 17 | 18 | [dev-dependencies] 19 | cosmwasm-schema = { version = "0.11.1" } 20 | -------------------------------------------------------------------------------- /packages/cw721/NOTICE: -------------------------------------------------------------------------------- 1 | CW721: A CosmWasm spec for non-fungible token contracts 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/cw721/README.md: -------------------------------------------------------------------------------- 1 | # CW721 Spec: Non Fungible Tokens 2 | 3 | CW721 is a specification for non-fungible tokens based on CosmWasm. 4 | The name and design is based on Ethereum's ERC721 standard, 5 | with some enhancements. The types in here can be imported by 6 | contracts that wish to implement this spec, or by contracts that call 7 | to any standard cw721 contract. 8 | 9 | The specification is split into multiple sections, a contract may only 10 | implement some of this functionality, but must implement the base. 11 | 12 | ## Base 13 | 14 | This handles ownership, transfers, and allowances. These must be supported 15 | as is by all CW721 contracts. Note that all tokens must have an owner, 16 | as well as an ID. The ID is an arbitrary string, unique within the contract. 17 | 18 | ### Messages 19 | 20 | `TransferNft{recipient, token_id}` - 21 | This transfers ownership of the token to `recipient` account. This is 22 | designed to send to an address controlled by a private key and *does not* 23 | trigger any actions on the recipient if it is a contract. 24 | 25 | Requires `token_id` to point to a valid token, and `env.sender` to be 26 | the owner of it, or have an allowance to transfer it. 27 | 28 | `SendNft{contract, token_id, msg}` - 29 | This transfers ownership of the token to `contract` account. `contract` 30 | must be an address controlled by a smart contract, which implements 31 | the CW721Receiver interface. The `msg` will be passed to the recipient 32 | contract, along with the token_id. 33 | 34 | Requires `token_id` to point to a valid token, and `env.sender` to be 35 | the owner of it, or have an allowance to transfer it. 36 | 37 | `Approve{spender, token_id, expires}` - Grants permission to `spender` to 38 | transfer or send the given token. This can only be performed when 39 | `env.sender` is the owner of the given `token_id` or an `operator`. 40 | There can multiple spender accounts per token, and they are cleared once 41 | the token is transfered or sent. 42 | 43 | `Revoke{spender, token_id}` - This revokes a previously granted permission 44 | to transfer the given `token_id`. This can only be granted when 45 | `env.sender` is the owner of the given `token_id` or an `operator`. 46 | 47 | `ApproveAll{operator, expires}` - Grant `operator` permission to transfer or send 48 | all tokens owned by `env.sender`. This approval is tied to the owner, not the 49 | tokens and applies to any future token that the owner receives as well. 50 | 51 | `RevokeAll{operator}` - Revoke a previous `ApproveAll` permission granted 52 | to the given `operator`. 53 | 54 | ### Queries 55 | 56 | `OwnerOf{token_id}` - Returns the owner of the given token, 57 | as well as anyone with approval on this particular token. 58 | If the token is unknown, returns an error. Return type is 59 | `OwnerResponse{owner}`. 60 | 61 | `ApprovedForAll{owner, include_expired}` - List all operators that can 62 | access all of the owner's tokens. Return type is `ApprovedForAllResponse`. 63 | If `include_expired` is set, show expired owners in the results, otherwise, 64 | ignore them. 65 | 66 | `NumTokens{}` - Total number of tokens issued 67 | 68 | ### Receiver 69 | 70 | The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by 71 | any contract that wishes to manage CW721 tokens. This is generally *not* 72 | implemented by any CW721 contract. 73 | 74 | `ReceiveNft{sender, token_id, msg}` - This is designed to handle `SendNft` 75 | messages. The address of the contract is stored in `env.sender` 76 | so it cannot be faked. The contract should ensure the sender matches 77 | the token contract it expects to handle, and not allow arbitrary addresses. 78 | 79 | The `sender` is the original account requesting to move the token 80 | and `msg` is a `Binary` data that can be decoded into a contract-specific 81 | message. This can be empty if we have only one default action, 82 | or it may be a `ReceiveMsg` variant to clarify the intention. For example, 83 | if I send to an exchange, I can specify the price I want to list the token 84 | for. 85 | 86 | ## Metadata 87 | 88 | ### Queries 89 | 90 | `ContractInfo{}` - This returns top-level metadata about the contract. 91 | Namely, `name` and `symbol`. 92 | 93 | `NftInfo{token_id}` - This returns metadata about one particular token. 94 | The return value is based on *ERC721 Metadata JSON Schema*, but directly 95 | from the contract, not as a Uri. Only the image link is a Uri. 96 | 97 | `AllNftInfo{token_id}` - This returns the result of both `NftInfo` 98 | and `OwnerOf` as one query as an optimization for clients, which may 99 | want both info to display one NFT. 100 | 101 | ## Enumerable 102 | 103 | ### Queries 104 | 105 | Pagination is acheived via `start_after` and `limit`. Limit is a request 106 | set by the client, if unset, the contract will automatically set it to 107 | `DefaultLimit` (suggested 10). If set, it will be used up to a `MaxLimit` 108 | value (suggested 30). Contracts can define other `DefaultLimit` and `MaxLimit` 109 | values without violating the CW721 spec, and clients should not rely on 110 | any particular values. 111 | 112 | If `start_after` is unset, the query returns the first results, ordered by 113 | lexogaphically by `token_id`. If `start_after` is set, then it returns the 114 | first `limit` tokens *after* the given one. This allows straight-forward 115 | pagination by taking the last result returned (a `token_id`) and using it 116 | as the `start_after` value in a future query. 117 | 118 | `Tokens{owner, start_after, limit}` - List all token_ids that belong to a given owner. 119 | Return type is `TokensResponse{tokens: Vec}`. 120 | 121 | `AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by 122 | the contract. 123 | -------------------------------------------------------------------------------- /packages/cw721/examples/schema.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::create_dir_all; 3 | 4 | use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; 5 | 6 | use cw721::{ 7 | AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721HandleMsg, 8 | Cw721QueryMsg, Cw721ReceiveMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, 9 | TokensResponse, 10 | }; 11 | 12 | fn main() { 13 | let mut out_dir = current_dir().unwrap(); 14 | out_dir.push("schema"); 15 | create_dir_all(&out_dir).unwrap(); 16 | remove_schemas(&out_dir).unwrap(); 17 | 18 | export_schema(&schema_for!(Cw721HandleMsg), &out_dir); 19 | export_schema(&schema_for!(Cw721QueryMsg), &out_dir); 20 | export_schema(&schema_for!(Cw721ReceiveMsg), &out_dir); 21 | export_schema(&schema_for!(AllNftInfoResponse), &out_dir); 22 | export_schema(&schema_for!(ApprovedForAllResponse), &out_dir); 23 | export_schema(&schema_for!(ContractInfoResponse), &out_dir); 24 | export_schema(&schema_for!(OwnerOfResponse), &out_dir); 25 | export_schema(&schema_for!(NftInfoResponse), &out_dir); 26 | export_schema(&schema_for!(NumTokensResponse), &out_dir); 27 | export_schema(&schema_for!(TokensResponse), &out_dir); 28 | } 29 | -------------------------------------------------------------------------------- /packages/cw721/schema/all_nft_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "AllNftInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "access", 7 | "info" 8 | ], 9 | "properties": { 10 | "access": { 11 | "description": "Who can transfer the token", 12 | "allOf": [ 13 | { 14 | "$ref": "#/definitions/OwnerOfResponse" 15 | } 16 | ] 17 | }, 18 | "info": { 19 | "description": "Data on the token itself,", 20 | "allOf": [ 21 | { 22 | "$ref": "#/definitions/NftInfoResponse" 23 | } 24 | ] 25 | } 26 | }, 27 | "definitions": { 28 | "Approval": { 29 | "type": "object", 30 | "required": [ 31 | "expires", 32 | "spender" 33 | ], 34 | "properties": { 35 | "expires": { 36 | "description": "When the Approval expires (maybe Expiration::never)", 37 | "allOf": [ 38 | { 39 | "$ref": "#/definitions/Expiration" 40 | } 41 | ] 42 | }, 43 | "spender": { 44 | "description": "Account that can transfer/send the token", 45 | "allOf": [ 46 | { 47 | "$ref": "#/definitions/HumanAddr" 48 | } 49 | ] 50 | } 51 | } 52 | }, 53 | "Expiration": { 54 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 55 | "anyOf": [ 56 | { 57 | "description": "AtHeight will expire when `env.block.height` >= height", 58 | "type": "object", 59 | "required": [ 60 | "at_height" 61 | ], 62 | "properties": { 63 | "at_height": { 64 | "type": "integer", 65 | "format": "uint64", 66 | "minimum": 0.0 67 | } 68 | } 69 | }, 70 | { 71 | "description": "AtTime will expire when `env.block.time` >= time", 72 | "type": "object", 73 | "required": [ 74 | "at_time" 75 | ], 76 | "properties": { 77 | "at_time": { 78 | "type": "integer", 79 | "format": "uint64", 80 | "minimum": 0.0 81 | } 82 | } 83 | }, 84 | { 85 | "description": "Never will never expire. Used to express the empty variant", 86 | "type": "object", 87 | "required": [ 88 | "never" 89 | ], 90 | "properties": { 91 | "never": { 92 | "type": "object" 93 | } 94 | } 95 | } 96 | ] 97 | }, 98 | "HumanAddr": { 99 | "type": "string" 100 | }, 101 | "NftInfoResponse": { 102 | "type": "object", 103 | "required": [ 104 | "description", 105 | "name" 106 | ], 107 | "properties": { 108 | "description": { 109 | "description": "Describes the asset to which this NFT represents", 110 | "type": "string" 111 | }, 112 | "image": { 113 | "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety", 114 | "type": [ 115 | "string", 116 | "null" 117 | ] 118 | }, 119 | "name": { 120 | "description": "Identifies the asset to which this NFT represents", 121 | "type": "string" 122 | } 123 | } 124 | }, 125 | "OwnerOfResponse": { 126 | "type": "object", 127 | "required": [ 128 | "approvals", 129 | "owner" 130 | ], 131 | "properties": { 132 | "approvals": { 133 | "description": "If set this address is approved to transfer/send the token as well", 134 | "type": "array", 135 | "items": { 136 | "$ref": "#/definitions/Approval" 137 | } 138 | }, 139 | "owner": { 140 | "description": "Owner of the token", 141 | "allOf": [ 142 | { 143 | "$ref": "#/definitions/HumanAddr" 144 | } 145 | ] 146 | } 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /packages/cw721/schema/approved_for_all_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ApprovedForAllResponse", 4 | "type": "object", 5 | "required": [ 6 | "operators" 7 | ], 8 | "properties": { 9 | "operators": { 10 | "type": "array", 11 | "items": { 12 | "$ref": "#/definitions/Approval" 13 | } 14 | } 15 | }, 16 | "definitions": { 17 | "Approval": { 18 | "type": "object", 19 | "required": [ 20 | "expires", 21 | "spender" 22 | ], 23 | "properties": { 24 | "expires": { 25 | "description": "When the Approval expires (maybe Expiration::never)", 26 | "allOf": [ 27 | { 28 | "$ref": "#/definitions/Expiration" 29 | } 30 | ] 31 | }, 32 | "spender": { 33 | "description": "Account that can transfer/send the token", 34 | "allOf": [ 35 | { 36 | "$ref": "#/definitions/HumanAddr" 37 | } 38 | ] 39 | } 40 | } 41 | }, 42 | "Expiration": { 43 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 44 | "anyOf": [ 45 | { 46 | "description": "AtHeight will expire when `env.block.height` >= height", 47 | "type": "object", 48 | "required": [ 49 | "at_height" 50 | ], 51 | "properties": { 52 | "at_height": { 53 | "type": "integer", 54 | "format": "uint64", 55 | "minimum": 0.0 56 | } 57 | } 58 | }, 59 | { 60 | "description": "AtTime will expire when `env.block.time` >= time", 61 | "type": "object", 62 | "required": [ 63 | "at_time" 64 | ], 65 | "properties": { 66 | "at_time": { 67 | "type": "integer", 68 | "format": "uint64", 69 | "minimum": 0.0 70 | } 71 | } 72 | }, 73 | { 74 | "description": "Never will never expire. Used to express the empty variant", 75 | "type": "object", 76 | "required": [ 77 | "never" 78 | ], 79 | "properties": { 80 | "never": { 81 | "type": "object" 82 | } 83 | } 84 | } 85 | ] 86 | }, 87 | "HumanAddr": { 88 | "type": "string" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/cw721/schema/contract_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "ContractInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "name", 7 | "symbol" 8 | ], 9 | "properties": { 10 | "name": { 11 | "type": "string" 12 | }, 13 | "symbol": { 14 | "type": "string" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cw721/schema/cw721_receive_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Cw721ReceiveMsg", 4 | "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg", 5 | "type": "object", 6 | "required": [ 7 | "sender", 8 | "token_id" 9 | ], 10 | "properties": { 11 | "msg": { 12 | "anyOf": [ 13 | { 14 | "$ref": "#/definitions/Binary" 15 | }, 16 | { 17 | "type": "null" 18 | } 19 | ] 20 | }, 21 | "sender": { 22 | "$ref": "#/definitions/HumanAddr" 23 | }, 24 | "token_id": { 25 | "type": "string" 26 | } 27 | }, 28 | "definitions": { 29 | "Binary": { 30 | "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", 31 | "type": "string" 32 | }, 33 | "HumanAddr": { 34 | "type": "string" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cw721/schema/nft_info_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "NftInfoResponse", 4 | "type": "object", 5 | "required": [ 6 | "description", 7 | "name" 8 | ], 9 | "properties": { 10 | "description": { 11 | "description": "Describes the asset to which this NFT represents", 12 | "type": "string" 13 | }, 14 | "image": { 15 | "description": "\"A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive. TODO: Use https://docs.rs/url_serde for type-safety", 16 | "type": [ 17 | "string", 18 | "null" 19 | ] 20 | }, 21 | "name": { 22 | "description": "Identifies the asset to which this NFT represents", 23 | "type": "string" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/cw721/schema/num_tokens_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "NumTokensResponse", 4 | "type": "object", 5 | "required": [ 6 | "count" 7 | ], 8 | "properties": { 9 | "count": { 10 | "type": "integer", 11 | "format": "uint64", 12 | "minimum": 0.0 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cw721/schema/owner_of_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "OwnerOfResponse", 4 | "type": "object", 5 | "required": [ 6 | "approvals", 7 | "owner" 8 | ], 9 | "properties": { 10 | "approvals": { 11 | "description": "If set this address is approved to transfer/send the token as well", 12 | "type": "array", 13 | "items": { 14 | "$ref": "#/definitions/Approval" 15 | } 16 | }, 17 | "owner": { 18 | "description": "Owner of the token", 19 | "allOf": [ 20 | { 21 | "$ref": "#/definitions/HumanAddr" 22 | } 23 | ] 24 | } 25 | }, 26 | "definitions": { 27 | "Approval": { 28 | "type": "object", 29 | "required": [ 30 | "expires", 31 | "spender" 32 | ], 33 | "properties": { 34 | "expires": { 35 | "description": "When the Approval expires (maybe Expiration::never)", 36 | "allOf": [ 37 | { 38 | "$ref": "#/definitions/Expiration" 39 | } 40 | ] 41 | }, 42 | "spender": { 43 | "description": "Account that can transfer/send the token", 44 | "allOf": [ 45 | { 46 | "$ref": "#/definitions/HumanAddr" 47 | } 48 | ] 49 | } 50 | } 51 | }, 52 | "Expiration": { 53 | "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", 54 | "anyOf": [ 55 | { 56 | "description": "AtHeight will expire when `env.block.height` >= height", 57 | "type": "object", 58 | "required": [ 59 | "at_height" 60 | ], 61 | "properties": { 62 | "at_height": { 63 | "type": "integer", 64 | "format": "uint64", 65 | "minimum": 0.0 66 | } 67 | } 68 | }, 69 | { 70 | "description": "AtTime will expire when `env.block.time` >= time", 71 | "type": "object", 72 | "required": [ 73 | "at_time" 74 | ], 75 | "properties": { 76 | "at_time": { 77 | "type": "integer", 78 | "format": "uint64", 79 | "minimum": 0.0 80 | } 81 | } 82 | }, 83 | { 84 | "description": "Never will never expire. Used to express the empty variant", 85 | "type": "object", 86 | "required": [ 87 | "never" 88 | ], 89 | "properties": { 90 | "never": { 91 | "type": "object" 92 | } 93 | } 94 | } 95 | ] 96 | }, 97 | "HumanAddr": { 98 | "type": "string" 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/cw721/schema/tokens_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "TokensResponse", 4 | "type": "object", 5 | "required": [ 6 | "tokens" 7 | ], 8 | "properties": { 9 | "tokens": { 10 | "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", 11 | "type": "array", 12 | "items": { 13 | "type": "string" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cw721/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{ 5 | to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, Querier, StdResult, WasmMsg, WasmQuery, 6 | }; 7 | 8 | use crate::{ 9 | AllNftInfoResponse, Approval, ApprovedForAllResponse, ContractInfoResponse, Cw721HandleMsg, 10 | Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, 11 | }; 12 | 13 | /// Cw721Contract is a wrapper around HumanAddr that provides a lot of helpers 14 | /// for working with this. 15 | /// 16 | /// If you wish to persist this, convert to Cw721CanonicalContract via .canonical() 17 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 18 | pub struct Cw721Contract(pub HumanAddr); 19 | 20 | impl Cw721Contract { 21 | pub fn addr(&self) -> HumanAddr { 22 | self.0.clone() 23 | } 24 | 25 | /// Convert this address to a form fit for storage 26 | pub fn canonical(&self, api: &A) -> StdResult { 27 | let canon = api.canonical_address(&self.0)?; 28 | Ok(Cw721CanonicalContract(canon)) 29 | } 30 | 31 | pub fn call(&self, msg: Cw721HandleMsg) -> StdResult { 32 | let msg = to_binary(&msg)?; 33 | Ok(WasmMsg::Execute { 34 | contract_addr: self.addr(), 35 | msg, 36 | send: vec![], 37 | } 38 | .into()) 39 | } 40 | 41 | pub fn query( 42 | &self, 43 | querier: &Q, 44 | req: Cw721QueryMsg, 45 | ) -> StdResult { 46 | let query = WasmQuery::Smart { 47 | contract_addr: self.addr(), 48 | msg: to_binary(&req)?, 49 | } 50 | .into(); 51 | querier.query(&query) 52 | } 53 | 54 | /*** queries ***/ 55 | 56 | pub fn owner_of>( 57 | &self, 58 | querier: &Q, 59 | token_id: T, 60 | include_expired: bool, 61 | ) -> StdResult { 62 | let req = Cw721QueryMsg::OwnerOf { 63 | token_id: token_id.into(), 64 | include_expired: Some(include_expired), 65 | }; 66 | self.query(querier, req) 67 | } 68 | 69 | pub fn approved_for_all>( 70 | &self, 71 | querier: &Q, 72 | owner: T, 73 | include_expired: bool, 74 | start_after: Option, 75 | limit: Option, 76 | ) -> StdResult> { 77 | let req = Cw721QueryMsg::ApprovedForAll { 78 | owner: owner.into(), 79 | include_expired: Some(include_expired), 80 | start_after, 81 | limit, 82 | }; 83 | let res: ApprovedForAllResponse = self.query(querier, req)?; 84 | Ok(res.operators) 85 | } 86 | 87 | pub fn num_tokens(&self, querier: &Q) -> StdResult { 88 | let req = Cw721QueryMsg::NumTokens {}; 89 | let res: NumTokensResponse = self.query(querier, req)?; 90 | Ok(res.count) 91 | } 92 | 93 | /// With metadata extension 94 | pub fn contract_info(&self, querier: &Q) -> StdResult { 95 | let req = Cw721QueryMsg::ContractInfo {}; 96 | self.query(querier, req) 97 | } 98 | 99 | /// With metadata extension 100 | pub fn nft_info>( 101 | &self, 102 | querier: &Q, 103 | token_id: T, 104 | ) -> StdResult { 105 | let req = Cw721QueryMsg::NftInfo { 106 | token_id: token_id.into(), 107 | }; 108 | self.query(querier, req) 109 | } 110 | 111 | /// With metadata extension 112 | pub fn all_nft_info>( 113 | &self, 114 | querier: &Q, 115 | token_id: T, 116 | include_expired: bool, 117 | ) -> StdResult { 118 | let req = Cw721QueryMsg::AllNftInfo { 119 | token_id: token_id.into(), 120 | include_expired: Some(include_expired), 121 | }; 122 | self.query(querier, req) 123 | } 124 | 125 | /// With enumerable extension 126 | pub fn tokens>( 127 | &self, 128 | querier: &Q, 129 | owner: T, 130 | start_after: Option, 131 | limit: Option, 132 | ) -> StdResult { 133 | let req = Cw721QueryMsg::Tokens { 134 | owner: owner.into(), 135 | start_after, 136 | limit, 137 | }; 138 | self.query(querier, req) 139 | } 140 | 141 | /// With enumerable extension 142 | pub fn all_tokens( 143 | &self, 144 | querier: &Q, 145 | start_after: Option, 146 | limit: Option, 147 | ) -> StdResult { 148 | let req = Cw721QueryMsg::AllTokens { start_after, limit }; 149 | self.query(querier, req) 150 | } 151 | 152 | /// returns true if the contract supports the metadata extension 153 | pub fn has_metadata(&self, querier: &Q) -> bool { 154 | self.contract_info(querier).is_ok() 155 | } 156 | 157 | /// returns true if the contract supports the enumerable extension 158 | pub fn has_enumerable(&self, querier: &Q) -> bool { 159 | self.tokens(querier, self.addr(), None, Some(1)).is_ok() 160 | } 161 | } 162 | 163 | /// This is a respresentation of Cw721Contract for storage. 164 | /// Don't use it directly, just translate to the Cw721Contract when needed. 165 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] 166 | pub struct Cw721CanonicalContract(pub CanonicalAddr); 167 | 168 | impl Cw721CanonicalContract { 169 | /// Convert this address to a form fit for usage in messages and queries 170 | pub fn human(&self, api: &A) -> StdResult { 171 | let human = api.human_address(&self.0)?; 172 | Ok(Cw721Contract(human)) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /packages/cw721/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | mod msg; 3 | mod query; 4 | mod receiver; 5 | 6 | pub use cw0::Expiration; 7 | 8 | pub use crate::helpers::{Cw721CanonicalContract, Cw721Contract}; 9 | pub use crate::msg::Cw721HandleMsg; 10 | pub use crate::query::{ 11 | AllNftInfoResponse, Approval, ApprovedForAllResponse, ContractInfoResponse, Cw721QueryMsg, 12 | NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, 13 | }; 14 | pub use crate::receiver::Cw721ReceiveMsg; 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | #[test] 19 | fn it_works() { 20 | assert_eq!(2 + 2, 4); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cw721/src/msg.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{Binary, HumanAddr}; 5 | use cw0::Expiration; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum Cw721HandleMsg { 10 | /// Transfer is a base message to move a token to another account without triggering actions 11 | TransferNft { 12 | recipient: HumanAddr, 13 | token_id: String, 14 | }, 15 | /// Send is a base message to transfer a token to a contract and trigger an action 16 | /// on the receiving contract. 17 | SendNft { 18 | contract: HumanAddr, 19 | token_id: String, 20 | msg: Option, 21 | }, 22 | /// Allows operator to transfer / send the token from the owner's account. 23 | /// If expiration is set, then this allowance has a time/height limit 24 | Approve { 25 | spender: HumanAddr, 26 | token_id: String, 27 | expires: Option, 28 | }, 29 | /// Remove previously granted Approval 30 | Revoke { 31 | spender: HumanAddr, 32 | token_id: String, 33 | }, 34 | /// Allows operator to transfer / send any token from the owner's account. 35 | /// If expiration is set, then this allowance has a time/height limit 36 | ApproveAll { 37 | operator: HumanAddr, 38 | expires: Option, 39 | }, 40 | /// Remove previously granted ApproveAll permission 41 | RevokeAll { operator: HumanAddr }, 42 | } 43 | -------------------------------------------------------------------------------- /packages/cw721/src/query.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::HumanAddr; 5 | use cw0::Expiration; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub enum Cw721QueryMsg { 10 | /// Return the owner of the given token, error if token does not exist 11 | /// Return type: OwnerOfResponse 12 | OwnerOf { 13 | token_id: String, 14 | /// unset or false will filter out expired approvals, you must set to true to see them 15 | include_expired: Option, 16 | }, 17 | /// List all operators that can access all of the owner's tokens. 18 | /// Return type: `ApprovedForAllResponse` 19 | ApprovedForAll { 20 | owner: HumanAddr, 21 | /// unset or false will filter out expired approvals, you must set to true to see them 22 | include_expired: Option, 23 | start_after: Option, 24 | limit: Option, 25 | }, 26 | /// Total number of tokens issued 27 | NumTokens {}, 28 | 29 | /// With MetaData Extension. 30 | /// Returns top-level metadata about the contract: `ContractInfoResponse` 31 | ContractInfo {}, 32 | /// With MetaData Extension. 33 | /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* 34 | /// but directly from the contract: `NftInfoResponse` 35 | NftInfo { token_id: String }, 36 | /// With MetaData Extension. 37 | /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization 38 | /// for clients: `AllNftInfo` 39 | AllNftInfo { 40 | token_id: String, 41 | /// unset or false will filter out expired approvals, you must set to true to see them 42 | include_expired: Option, 43 | }, 44 | 45 | /// With Enumerable extension. 46 | /// Returns all tokens owned by the given address, [] if unset. 47 | /// Return type: TokensResponse. 48 | Tokens { 49 | owner: HumanAddr, 50 | start_after: Option, 51 | limit: Option, 52 | }, 53 | /// With Enumerable extension. 54 | /// Requires pagination. Lists all token_ids controlled by the contract. 55 | /// Return type: TokensResponse. 56 | AllTokens { 57 | start_after: Option, 58 | limit: Option, 59 | }, 60 | } 61 | 62 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 63 | pub struct OwnerOfResponse { 64 | /// Owner of the token 65 | pub owner: HumanAddr, 66 | /// If set this address is approved to transfer/send the token as well 67 | pub approvals: Vec, 68 | } 69 | 70 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 71 | pub struct Approval { 72 | /// Account that can transfer/send the token 73 | pub spender: HumanAddr, 74 | /// When the Approval expires (maybe Expiration::never) 75 | pub expires: Expiration, 76 | } 77 | 78 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 79 | pub struct ApprovedForAllResponse { 80 | pub operators: Vec, 81 | } 82 | 83 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 84 | pub struct NumTokensResponse { 85 | pub count: u64, 86 | } 87 | 88 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 89 | pub struct ContractInfoResponse { 90 | pub name: String, 91 | pub symbol: String, 92 | } 93 | 94 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 95 | pub struct NftInfoResponse { 96 | /// Identifies the asset to which this NFT represents 97 | pub name: String, 98 | /// Describes Monster level 99 | pub level: u64, 100 | /// Describes the asset to which this NFT represents 101 | pub description: String, 102 | /// "A URI pointing to a resource with mime type image/* representing the asset to which this 103 | /// NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect 104 | /// ratio between 1.91:1 and 4:5 inclusive. 105 | /// TODO: Use https://docs.rs/url_serde for type-safety 106 | pub image: Option, 107 | } 108 | 109 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 110 | pub struct AllNftInfoResponse { 111 | /// Who can transfer the token 112 | pub access: OwnerOfResponse, 113 | /// Data on the token itself, 114 | pub info: NftInfoResponse, 115 | } 116 | 117 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 118 | pub struct TokensResponse { 119 | /// Contains all token_ids in lexicographical ordering 120 | /// If there are more than `limit`, use `start_from` in future queries 121 | /// to achieve pagination. 122 | pub tokens: Vec, 123 | } 124 | -------------------------------------------------------------------------------- /packages/cw721/src/receiver.rs: -------------------------------------------------------------------------------- 1 | use schemars::JsonSchema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use cosmwasm_std::{to_binary, Binary, CosmosMsg, HumanAddr, StdResult, WasmMsg}; 5 | 6 | /// Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 8 | #[serde(rename_all = "snake_case")] 9 | pub struct Cw721ReceiveMsg { 10 | pub sender: HumanAddr, 11 | pub token_id: String, 12 | pub msg: Option, 13 | } 14 | 15 | impl Cw721ReceiveMsg { 16 | /// serializes the message 17 | pub fn into_binary(self) -> StdResult { 18 | let msg = ReceiverHandleMsg::ReceiveNft(self); 19 | to_binary(&msg) 20 | } 21 | 22 | /// creates a cosmos_msg sending this struct to the named contract 23 | pub fn into_cosmos_msg(self, contract_addr: HumanAddr) -> StdResult { 24 | let msg = self.into_binary()?; 25 | let execute = WasmMsg::Execute { 26 | contract_addr, 27 | msg, 28 | send: vec![], 29 | }; 30 | Ok(execute.into()) 31 | } 32 | } 33 | 34 | /// This is just a helper to properly serialize the above message. 35 | /// The actual receiver should include this variant in the larger HandleMsg enum 36 | #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] 37 | #[serde(rename_all = "snake_case")] 38 | enum ReceiverHandleMsg { 39 | ReceiveNft(Cw721ReceiveMsg), 40 | } 41 | -------------------------------------------------------------------------------- /packages/storage-plus/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cw-storage-plus" 3 | version = "0.3.2" 4 | authors = ["Ethan Frey "] 5 | edition = "2018" 6 | description = "Enhanced/experimental storage engines" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/CosmWasm/cosmwasm-plus" 9 | homepage = "https://cosmwasm.com" 10 | documentation = "https://docs.cosmwasm.com" 11 | 12 | [features] 13 | iterator = ["cosmwasm-std/iterator"] 14 | 15 | [dependencies] 16 | cosmwasm-std = { version = "0.11.1" } 17 | schemars = "0.7" 18 | serde = { version = "1.0.103", default-features = false, features = ["derive"] } 19 | -------------------------------------------------------------------------------- /packages/storage-plus/NOTICE: -------------------------------------------------------------------------------- 1 | CW-Storage-Plus: Enhanced/experimental storage engines for CosmWasm 2 | Copyright (C) 2020 Confio OÜ 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /packages/storage-plus/src/endian.rs: -------------------------------------------------------------------------------- 1 | //! This code is inspired by (and partially borrowed from) 2 | //! https://docs.rs/endiannezz/0.5.2/endiannezz/trait.Primitive.html 3 | //! but there was a lot in that crate I did not want, the name did not inspire 4 | //! confidence, and I wanted a different return value, so I just took the code 5 | //! to modify slightly. 6 | 7 | // TODO: figure out these macros and let us replace (self: Self) with self 8 | #![allow(clippy::needless_arbitrary_self_type)] 9 | 10 | use std::mem; 11 | 12 | pub trait Endian: Sized + Copy { 13 | type Buf: AsRef<[u8]> + AsMut<[u8]> + Into> + Default; 14 | 15 | fn to_le_bytes(self) -> Self::Buf; 16 | fn to_be_bytes(self) -> Self::Buf; 17 | 18 | fn from_le_bytes(bytes: Self::Buf) -> Self; 19 | fn from_be_bytes(bytes: Self::Buf) -> Self; 20 | } 21 | 22 | macro_rules! delegate { 23 | ($ty:ty, [$($method:ident),* $(,)?], ($param:ident : $param_ty:ty) -> $ret:ty) => { 24 | delegate!(@inner $ty, [$($method),*], $param, $param_ty, $ret); 25 | }; 26 | (@inner $ty:ty, [$($method:ident),*], $param:ident, $param_ty:ty, $ret:ty) => { 27 | $( 28 | #[inline] 29 | fn $method ($param: $param_ty) -> $ret { <$ty>::$method($param) } 30 | )* 31 | }; 32 | } 33 | 34 | macro_rules! impl_primitives { 35 | ($($ty:ty),* $(,)?) => { 36 | $( 37 | impl Endian for $ty { 38 | type Buf = [u8; mem::size_of::<$ty>()]; 39 | 40 | delegate!($ty, [ 41 | to_le_bytes, 42 | to_be_bytes, 43 | ], (self: Self) -> Self::Buf); 44 | 45 | delegate!($ty, [ 46 | from_le_bytes, 47 | from_be_bytes, 48 | ], (bytes: Self::Buf) -> Self); 49 | } 50 | )* 51 | }; 52 | } 53 | 54 | #[rustfmt::skip] 55 | impl_primitives![ 56 | i8, i16, i32, i64, i128, 57 | u8, u16, u32, u64, u128, 58 | ]; 59 | -------------------------------------------------------------------------------- /packages/storage-plus/src/helpers.rs: -------------------------------------------------------------------------------- 1 | //! This module is an implemention of a namespacing scheme described 2 | //! in https://github.com/webmaster128/key-namespacing#length-prefixed-keys 3 | //! 4 | //! Everything in this file is only responsible for building such keys 5 | //! and is in no way specific to any kind of storage. 6 | 7 | use serde::de::DeserializeOwned; 8 | use std::any::type_name; 9 | 10 | use cosmwasm_std::{from_slice, StdError, StdResult}; 11 | 12 | /// may_deserialize parses json bytes from storage (Option), returning Ok(None) if no data present 13 | /// 14 | /// value is an odd type, but this is meant to be easy to use with output from storage.get (Option>) 15 | /// and value.map(|s| s.as_slice()) seems trickier than &value 16 | pub(crate) fn may_deserialize( 17 | value: &Option>, 18 | ) -> StdResult> { 19 | match value { 20 | Some(vec) => Ok(Some(from_slice(&vec)?)), 21 | None => Ok(None), 22 | } 23 | } 24 | 25 | /// must_deserialize parses json bytes from storage (Option), returning NotFound error if no data present 26 | pub(crate) fn must_deserialize(value: &Option>) -> StdResult { 27 | match value { 28 | Some(vec) => from_slice(&vec), 29 | None => Err(StdError::not_found(type_name::())), 30 | } 31 | } 32 | 33 | /// This is equivalent concat(to_length_prefixed_nested(namespaces), key) 34 | /// But more efficient when the intermediate namespaces often must be recalculated 35 | pub(crate) fn namespaces_with_key(namespaces: &[&[u8]], key: &[u8]) -> Vec { 36 | let mut size = key.len(); 37 | for &namespace in namespaces { 38 | size += namespace.len() + 2; 39 | } 40 | 41 | let mut out = Vec::with_capacity(size); 42 | for &namespace in namespaces { 43 | out.extend_from_slice(&encode_length(namespace)); 44 | out.extend_from_slice(namespace); 45 | } 46 | out.extend_from_slice(key); 47 | out 48 | } 49 | 50 | /// Customization of namespaces_with_key for when 51 | /// there are multiple sets we do not want to combine just to call this 52 | pub(crate) fn nested_namespaces_with_key( 53 | top_names: &[&[u8]], 54 | sub_names: &[&[u8]], 55 | key: &[u8], 56 | ) -> Vec { 57 | let mut size = key.len(); 58 | for &namespace in top_names { 59 | size += namespace.len() + 2; 60 | } 61 | for &namespace in sub_names { 62 | size += namespace.len() + 2; 63 | } 64 | 65 | let mut out = Vec::with_capacity(size); 66 | for &namespace in top_names { 67 | out.extend_from_slice(&encode_length(namespace)); 68 | out.extend_from_slice(namespace); 69 | } 70 | for &namespace in sub_names { 71 | out.extend_from_slice(&encode_length(namespace)); 72 | out.extend_from_slice(namespace); 73 | } 74 | out.extend_from_slice(key); 75 | out 76 | } 77 | 78 | /// Encodes the length of a given namespace as a 2 byte big endian encoded integer 79 | pub(crate) fn encode_length(namespace: &[u8]) -> [u8; 2] { 80 | if namespace.len() > 0xFFFF { 81 | panic!("only supports namespaces up to length 0xFFFF") 82 | } 83 | let length_bytes = (namespace.len() as u32).to_be_bytes(); 84 | [length_bytes[2], length_bytes[3]] 85 | } 86 | 87 | // pub(crate) fn decode_length(prefix: [u8; 2]) -> usize { 88 | pub(crate) fn decode_length(prefix: &[u8]) -> usize { 89 | // TODO: enforce exactly 2 bytes somehow, but usable with slices 90 | (prefix[0] as usize) * 256 + (prefix[1] as usize) 91 | } 92 | 93 | #[cfg(test)] 94 | mod test { 95 | use super::*; 96 | use cosmwasm_std::{to_vec, StdError}; 97 | use serde::{Deserialize, Serialize}; 98 | 99 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 100 | struct Person { 101 | pub name: String, 102 | pub age: i32, 103 | } 104 | 105 | #[test] 106 | fn encode_length_works() { 107 | assert_eq!(encode_length(b""), *b"\x00\x00"); 108 | assert_eq!(encode_length(b"a"), *b"\x00\x01"); 109 | assert_eq!(encode_length(b"aa"), *b"\x00\x02"); 110 | assert_eq!(encode_length(b"aaa"), *b"\x00\x03"); 111 | assert_eq!(encode_length(&vec![1; 255]), *b"\x00\xff"); 112 | assert_eq!(encode_length(&vec![1; 256]), *b"\x01\x00"); 113 | assert_eq!(encode_length(&vec![1; 12345]), *b"\x30\x39"); 114 | assert_eq!(encode_length(&vec![1; 65535]), *b"\xff\xff"); 115 | } 116 | 117 | #[test] 118 | #[should_panic(expected = "only supports namespaces up to length 0xFFFF")] 119 | fn encode_length_panics_for_large_values() { 120 | encode_length(&vec![1; 65536]); 121 | } 122 | 123 | #[test] 124 | fn may_deserialize_handles_some() { 125 | let person = Person { 126 | name: "Maria".to_string(), 127 | age: 42, 128 | }; 129 | let value = to_vec(&person).unwrap(); 130 | 131 | let may_parse: Option = may_deserialize(&Some(value)).unwrap(); 132 | assert_eq!(may_parse, Some(person)); 133 | } 134 | 135 | #[test] 136 | fn may_deserialize_handles_none() { 137 | let may_parse = may_deserialize::(&None).unwrap(); 138 | assert_eq!(may_parse, None); 139 | } 140 | 141 | #[test] 142 | fn must_deserialize_handles_some() { 143 | let person = Person { 144 | name: "Maria".to_string(), 145 | age: 42, 146 | }; 147 | let value = to_vec(&person).unwrap(); 148 | let loaded = Some(value); 149 | 150 | let parsed: Person = must_deserialize(&loaded).unwrap(); 151 | assert_eq!(parsed, person); 152 | } 153 | 154 | #[test] 155 | fn must_deserialize_handles_none() { 156 | let parsed = must_deserialize::(&None); 157 | match parsed.unwrap_err() { 158 | StdError::NotFound { kind, .. } => { 159 | assert_eq!(kind, "cw_storage_plus::helpers::test::Person") 160 | } 161 | e => panic!("Unexpected error {}", e), 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /packages/storage-plus/src/legacy_helpers.rs: -------------------------------------------------------------------------------- 1 | // This code is intentionally included not in lib.rs 2 | // Most of it will be deleted. But maybe we want to borrow some chunks, so keeping them here. 3 | 4 | /// Calculates the raw key prefix for a given nested namespace 5 | /// as documented in https://github.com/webmaster128/key-namespacing#nesting 6 | pub(crate) fn to_length_prefixed_nested(namespaces: &[&[u8]]) -> Vec { 7 | let mut size = 0; 8 | for &namespace in namespaces { 9 | size += namespace.len() + 2; 10 | } 11 | 12 | let mut out = Vec::with_capacity(size); 13 | for &namespace in namespaces { 14 | out.extend_from_slice(&encode_length(namespace)); 15 | out.extend_from_slice(namespace); 16 | } 17 | out 18 | } 19 | 20 | pub(crate) fn length_prefixed_with_key(namespace: &[u8], key: &[u8]) -> Vec { 21 | let mut out = Vec::with_capacity(namespace.len() + 2 + key.len()); 22 | out.extend_from_slice(&encode_length(namespace)); 23 | out.extend_from_slice(namespace); 24 | out.extend_from_slice(key); 25 | out 26 | } 27 | 28 | pub(crate) fn get_with_prefix( 29 | storage: &S, 30 | namespace: &[u8], 31 | key: &[u8], 32 | ) -> Option> { 33 | storage.get(&concat(namespace, key)) 34 | } 35 | 36 | pub(crate) fn set_with_prefix( 37 | storage: &mut S, 38 | namespace: &[u8], 39 | key: &[u8], 40 | value: &[u8], 41 | ) { 42 | storage.set(&concat(namespace, key), value); 43 | } 44 | 45 | pub(crate) fn remove_with_prefix(storage: &mut S, namespace: &[u8], key: &[u8]) { 46 | storage.remove(&concat(namespace, key)); 47 | } 48 | 49 | #[cfg(test)] 50 | mod legacy_test { 51 | use super::*; 52 | use crate::helpers::*; 53 | use cosmwasm_std::testing::MockStorage; 54 | 55 | #[test] 56 | fn to_length_prefixed_nested_works() { 57 | assert_eq!(to_length_prefixed_nested(&[]), b""); 58 | assert_eq!(to_length_prefixed_nested(&[b""]), b"\x00\x00"); 59 | assert_eq!(to_length_prefixed_nested(&[b"", b""]), b"\x00\x00\x00\x00"); 60 | 61 | assert_eq!(to_length_prefixed_nested(&[b"a"]), b"\x00\x01a"); 62 | assert_eq!( 63 | to_length_prefixed_nested(&[b"a", b"ab"]), 64 | b"\x00\x01a\x00\x02ab" 65 | ); 66 | assert_eq!( 67 | to_length_prefixed_nested(&[b"a", b"ab", b"abc"]), 68 | b"\x00\x01a\x00\x02ab\x00\x03abc" 69 | ); 70 | } 71 | 72 | #[test] 73 | fn to_length_prefixed_nested_allows_many_long_namespaces() { 74 | // The 0xFFFF limit is for each namespace, not for the combination of them 75 | 76 | let long_namespace1 = vec![0xaa; 0xFFFD]; 77 | let long_namespace2 = vec![0xbb; 0xFFFE]; 78 | let long_namespace3 = vec![0xcc; 0xFFFF]; 79 | 80 | let prefix = 81 | to_length_prefixed_nested(&[&long_namespace1, &long_namespace2, &long_namespace3]); 82 | assert_eq!(&prefix[0..2], b"\xFF\xFD"); 83 | assert_eq!(&prefix[2..(2 + 0xFFFD)], long_namespace1.as_slice()); 84 | assert_eq!(&prefix[(2 + 0xFFFD)..(2 + 0xFFFD + 2)], b"\xFF\xFe"); 85 | assert_eq!( 86 | &prefix[(2 + 0xFFFD + 2)..(2 + 0xFFFD + 2 + 0xFFFE)], 87 | long_namespace2.as_slice() 88 | ); 89 | assert_eq!( 90 | &prefix[(2 + 0xFFFD + 2 + 0xFFFE)..(2 + 0xFFFD + 2 + 0xFFFE + 2)], 91 | b"\xFF\xFf" 92 | ); 93 | assert_eq!( 94 | &prefix[(2 + 0xFFFD + 2 + 0xFFFE + 2)..(2 + 0xFFFD + 2 + 0xFFFE + 2 + 0xFFFF)], 95 | long_namespace3.as_slice() 96 | ); 97 | } 98 | 99 | #[test] 100 | fn to_length_prefixed_nested_calculates_capacity_correctly() { 101 | // Those tests cannot guarantee the required capacity was calculated correctly before 102 | // the vector allocation but increase the likelyhood of a proper implementation. 103 | 104 | let key = to_length_prefixed_nested(&[]); 105 | assert_eq!(key.capacity(), key.len()); 106 | 107 | let key = to_length_prefixed_nested(&[b""]); 108 | assert_eq!(key.capacity(), key.len()); 109 | 110 | let key = to_length_prefixed_nested(&[b"a"]); 111 | assert_eq!(key.capacity(), key.len()); 112 | 113 | let key = to_length_prefixed_nested(&[b"a", b"bc"]); 114 | assert_eq!(key.capacity(), key.len()); 115 | 116 | let key = to_length_prefixed_nested(&[b"a", b"bc", b"def"]); 117 | assert_eq!(key.capacity(), key.len()); 118 | } 119 | 120 | 121 | #[test] 122 | fn prefix_get_set() { 123 | let mut storage = MockStorage::new(); 124 | let prefix = to_length_prefixed(b"foo"); 125 | 126 | set_with_prefix(&mut storage, &prefix, b"bar", b"gotcha"); 127 | let rfoo = get_with_prefix(&storage, &prefix, b"bar"); 128 | assert_eq!(rfoo, Some(b"gotcha".to_vec())); 129 | 130 | // no collisions with other prefixes 131 | let other_prefix = to_length_prefixed(b"fo"); 132 | let collision = get_with_prefix(&storage, &other_prefix, b"obar"); 133 | assert_eq!(collision, None); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /packages/storage-plus/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod endian; 2 | mod helpers; 3 | mod indexed_map; 4 | mod indexes; 5 | mod item; 6 | mod iter_helpers; 7 | mod keys; 8 | mod map; 9 | mod path; 10 | mod prefix; 11 | 12 | pub use endian::Endian; 13 | #[cfg(feature = "iterator")] 14 | pub use indexed_map::{IndexList, IndexedMap}; 15 | #[cfg(feature = "iterator")] 16 | pub use indexes::{index_int, index_string, Index, MultiIndex, UniqueIndex}; 17 | pub use item::Item; 18 | pub use keys::{PkOwned, Prefixer, PrimaryKey, U128Key, U16Key, U32Key, U64Key}; 19 | pub use map::Map; 20 | pub use path::Path; 21 | #[cfg(feature = "iterator")] 22 | pub use prefix::{Bound, Prefix}; 23 | -------------------------------------------------------------------------------- /packages/storage-plus/src/path.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | use serde::Serialize; 3 | use std::marker::PhantomData; 4 | 5 | use crate::helpers::{may_deserialize, must_deserialize, nested_namespaces_with_key}; 6 | use cosmwasm_std::{to_vec, StdError, StdResult, Storage}; 7 | use std::ops::Deref; 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct Path 11 | where 12 | T: Serialize + DeserializeOwned, 13 | { 14 | /// all namespaces prefixes and concatenated with the key 15 | storage_key: Vec, 16 | // see https://doc.rust-lang.org/std/marker/struct.PhantomData.html#unused-type-parameters for why this is needed 17 | data: PhantomData, 18 | } 19 | 20 | impl Deref for Path 21 | where 22 | T: Serialize + DeserializeOwned, 23 | { 24 | type Target = [u8]; 25 | 26 | fn deref(&self) -> &[u8] { 27 | &self.storage_key 28 | } 29 | } 30 | 31 | impl Path 32 | where 33 | T: Serialize + DeserializeOwned, 34 | { 35 | pub fn new(namespace: &[u8], keys: &[&[u8]]) -> Self { 36 | let l = keys.len(); 37 | // FIXME: make this more efficient 38 | let storage_key = nested_namespaces_with_key(&[namespace], &keys[0..l - 1], keys[l - 1]); 39 | Path { 40 | storage_key, 41 | data: PhantomData, 42 | } 43 | } 44 | 45 | /// save will serialize the model and store, returns an error on serialization issues 46 | pub fn save(&self, store: &mut S, data: &T) -> StdResult<()> { 47 | store.set(&self.storage_key, &to_vec(data)?); 48 | Ok(()) 49 | } 50 | 51 | pub fn remove(&self, store: &mut S) { 52 | store.remove(&self.storage_key); 53 | } 54 | 55 | /// load will return an error if no data is set at the given key, or on parse error 56 | pub fn load(&self, store: &S) -> StdResult { 57 | let value = store.get(&self.storage_key); 58 | must_deserialize(&value) 59 | } 60 | 61 | /// may_load will parse the data stored at the key if present, returns Ok(None) if no data there. 62 | /// returns an error on issues parsing 63 | pub fn may_load(&self, store: &S) -> StdResult> { 64 | let value = store.get(&self.storage_key); 65 | may_deserialize(&value) 66 | } 67 | 68 | /// Loads the data, perform the specified action, and store the result 69 | /// in the database. This is shorthand for some common sequences, which may be useful. 70 | /// 71 | /// If the data exists, `action(Some(value))` is called. Otherwise `action(None)` is called. 72 | pub fn update(&self, store: &mut S, action: A) -> Result 73 | where 74 | A: FnOnce(Option) -> Result, 75 | E: From, 76 | S: Storage, 77 | { 78 | let input = self.may_load(store)?; 79 | let output = action(input)?; 80 | self.save(store, &output)?; 81 | Ok(output) 82 | } 83 | } 84 | --------------------------------------------------------------------------------