From 1cc6de79ee14a9eba52e11ea9596d17912bfdcdc Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 20 Aug 2020 23:00:03 +0200 Subject: [PATCH 01/10] Write README for cw721 package --- packages/cw721/.cargo/config | 4 ++ packages/cw721/Cargo.toml | 15 ++++++ packages/cw721/NOTICE | 14 ++++++ packages/cw721/README.md | 90 ++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 packages/cw721/.cargo/config create mode 100644 packages/cw721/Cargo.toml create mode 100644 packages/cw721/NOTICE create mode 100644 packages/cw721/README.md diff --git a/packages/cw721/.cargo/config b/packages/cw721/.cargo/config new file mode 100644 index 000000000..b613a59f1 --- /dev/null +++ b/packages/cw721/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +schema = "run --example schema" diff --git a/packages/cw721/Cargo.toml b/packages/cw721/Cargo.toml new file mode 100644 index 000000000..37b29822e --- /dev/null +++ b/packages/cw721/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cw721" +version = "0.1.1" +authors = ["Ethan Frey "] +edition = "2018" +description = "Definition and types for the CosmWasm-721 NFT interface" +license = "Apache-2.0" + +[dependencies] +cosmwasm-std = { version = "0.10.0" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +[dev-dependencies] +cosmwasm-schema = { version = "0.10.0" } diff --git a/packages/cw721/NOTICE b/packages/cw721/NOTICE new file mode 100644 index 000000000..bf1aac02a --- /dev/null +++ b/packages/cw721/NOTICE @@ -0,0 +1,14 @@ +CW721: A CosmWasm spec for non-fungible token contracts +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/cw721/README.md b/packages/cw721/README.md new file mode 100644 index 000000000..45df10f7e --- /dev/null +++ b/packages/cw721/README.md @@ -0,0 +1,90 @@ +# CW721 Spec: Non Fungible Tokens + +CW721 is a specification for fungible tokens based on CosmWasm. +The name and design is based on Ethereum's ERC721 standard, +with some enhancements. The types in here can be imported by +contracts that wish to implement this spec, or by contracts that call +to any standard cw721 contract. + +The specification is split into multiple sections, a contract may only +implement some of this functionality, but must implement the base. + +## Base + +This handles ownership, transfers, and allowances. These must be supported +as is by all CW721 contracts. Note that all tokens must have an owner, +as well as an ID. The ID is an arbitrary string, unique within the contract. + +### Messages + +`TransferNft{recipient, token_id}` - +This transfers ownership of the token to `recipient` account. This is +designed to send to an address controlled by a private key and *does not* +trigger any actions on the recipient if it is a contract. + +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. + +`SendNft{contract, token_id, msg}` - +This transfers ownership of the token to `contract` account. `contract` +must be an address controlled by a smart contract, which implements +the CW721Receiver interface. The `msg` will be passed to the recipient +contract, along with the token_id. + +Requires `token_id` to point to a valid token, and `env.sender` to be +the owner of it, or have an allowance to transfer it. + +`Approve{operator, token_id}` - Grants permission to `operator` to +transfer or send the given token. This can only be granted when +`env.sender` is the owner of the given `token_id`. All approvals +are cleared once the token is transfered. + +`Revoke{operator, token_id}` - This revokes a previously granted permission +to transfer the given `token_id`. This can only be granted when +`env.sender` is the owner of the given `token_id`. + +`ApproveAll{operator}` - Grant `operator` permission to transfer or send +all tokens owner by `env.sender`. This is tied to the owner, not the +tokens and applies to any future token that the owner receives as well. + +`RevokeAll{operator}` - Revoke a previous `ApproveAll` permission granted +to the given `operator`. + +### Queries + +`OwnerOf{token_id}` - Returns the owner of the given token. +If the token is unknown, returns an error. Return type is +`OwnerResponse{owner}` with a bech32 address. + +**TODO** Do we want to be this ambitious? Handle paging? +Right now, let it just get big and expensive to query + +`Tokens{owner}` - List all token_ids that belong to a given owner. +Return type is `TokensResponse{tokens: Vec}`. + +### Receiver + +The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by +any contract that wishes to manage CW721 tokens. This is generally *not* +implemented by any CW721 contract. + +`ReceiveNft{sender, token_id, msg}` - This is designed to handle `SendNft` +messages. The address of the contract is stored in `env.sender` +so it cannot be faked. The contract should ensure the sender matches +the token contract it expects to handle, and not allow arbitrary addresses. + +The `sender` is the original account requesting to move the token +and `msg` is a `Binary` data that can be decoded into a contract-specific +message. This can be empty if we have only one default action, +or it may be a `ReceiveMsg` variant to clarify the intention. For example, +if I send to an exchange, I can specify the price I want to list the token +for. + +## Mintable + +Can we generalize this??? + +### Query With Data + +Can we generalize the data types??? + From ca04bb6ec38ba334fa4891137681308e3c663ecf Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 20 Aug 2020 23:21:32 +0200 Subject: [PATCH 02/10] Implement basic HandleMsg and QueryMsg types --- Cargo.lock | 10 + packages/cw721/README.md | 23 +- packages/cw721/examples/schema.rs | 22 ++ .../schema/approved_for_all_response.json | 21 ++ packages/cw721/schema/cw721_handle_msg.json | 225 ++++++++++++++++++ packages/cw721/schema/cw721_query_msg.json | 69 ++++++ packages/cw721/schema/owner_of_response.json | 87 +++++++ packages/cw721/schema/tokens_response.json | 16 ++ packages/cw721/src/lib.rs | 15 ++ packages/cw721/src/msg.rs | 69 ++++++ packages/cw721/src/query.rs | 48 ++++ 11 files changed, 596 insertions(+), 9 deletions(-) create mode 100644 packages/cw721/examples/schema.rs create mode 100644 packages/cw721/schema/approved_for_all_response.json create mode 100644 packages/cw721/schema/cw721_handle_msg.json create mode 100644 packages/cw721/schema/cw721_query_msg.json create mode 100644 packages/cw721/schema/owner_of_response.json create mode 100644 packages/cw721/schema/tokens_response.json create mode 100644 packages/cw721/src/lib.rs create mode 100644 packages/cw721/src/msg.rs create mode 100644 packages/cw721/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index b04c687d5..3339e90e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,16 @@ dependencies = [ "snafu", ] +[[package]] +name = "cw721" +version = "0.1.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "doc-comment" version = "0.3.3" diff --git a/packages/cw721/README.md b/packages/cw721/README.md index 45df10f7e..c428b55dc 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -34,16 +34,17 @@ contract, along with the token_id. Requires `token_id` to point to a valid token, and `env.sender` to be the owner of it, or have an allowance to transfer it. -`Approve{operator, token_id}` - Grants permission to `operator` to -transfer or send the given token. This can only be granted when -`env.sender` is the owner of the given `token_id`. All approvals -are cleared once the token is transfered. +`Approve{approved, token_id, expires}` - Grants permission to `approved` to +transfer or send the given token. This can only be performed when +`env.sender` is the owner of the given `token_id` or an `operator`. +There can only be one approved account per token, and it is cleared once +the token is transfered or sent. -`Revoke{operator, token_id}` - This revokes a previously granted permission +`Revoke{token_id}` - This revokes a previously granted permission to transfer the given `token_id`. This can only be granted when -`env.sender` is the owner of the given `token_id`. +`env.sender` is the owner of the given `token_id` or an `operator`. -`ApproveAll{operator}` - Grant `operator` permission to transfer or send +`ApproveAll{operator, expires}` - Grant `operator` permission to transfer or send all tokens owner by `env.sender`. This is tied to the owner, not the tokens and applies to any future token that the owner receives as well. @@ -52,9 +53,10 @@ to the given `operator`. ### Queries -`OwnerOf{token_id}` - Returns the owner of the given token. +`OwnerOf{token_id}` - Returns the owner of the given token, +as well as anyone with approval on this particular token. If the token is unknown, returns an error. Return type is -`OwnerResponse{owner}` with a bech32 address. +`OwnerResponse{owner}`. **TODO** Do we want to be this ambitious? Handle paging? Right now, let it just get big and expensive to query @@ -62,6 +64,9 @@ Right now, let it just get big and expensive to query `Tokens{owner}` - List all token_ids that belong to a given owner. Return type is `TokensResponse{tokens: Vec}`. +`ApprovedForAll{owner}` - List all operators that can access all of +the owner's tokens. Return type is `ApprovedForAllResponse` + ### Receiver The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by diff --git a/packages/cw721/examples/schema.rs b/packages/cw721/examples/schema.rs new file mode 100644 index 000000000..76fac8272 --- /dev/null +++ b/packages/cw721/examples/schema.rs @@ -0,0 +1,22 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +use cw721::{ + ApprovedForAllResponse, Cw721HandleMsg, Cw721QueryMsg, OwnerOfResponse, TokensResponse, +}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(Cw721HandleMsg), &out_dir); + export_schema(&schema_for!(Cw721QueryMsg), &out_dir); + // export_schema(&schema_for!(Cw20ReceiveMsg), &out_dir); + export_schema(&schema_for!(ApprovedForAllResponse), &out_dir); + export_schema(&schema_for!(OwnerOfResponse), &out_dir); + export_schema(&schema_for!(TokensResponse), &out_dir); +} diff --git a/packages/cw721/schema/approved_for_all_response.json b/packages/cw721/schema/approved_for_all_response.json new file mode 100644 index 000000000..9fc643c72 --- /dev/null +++ b/packages/cw721/schema/approved_for_all_response.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovedForAllResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/HumanAddr" + } + } + }, + "definitions": { + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/cw721_handle_msg.json b/packages/cw721/schema/cw721_handle_msg.json new file mode 100644 index 000000000..858db43dc --- /dev/null +++ b/packages/cw721/schema/cw721_handle_msg.json @@ -0,0 +1,225 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721HandleMsg", + "anyOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "$ref": "#/definitions/HumanAddr" + }, + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "token_id" + ], + "properties": { + "contract": { + "$ref": "#/definitions/HumanAddr" + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "operator", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "$ref": "#/definitions/HumanAddr" + }, + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "operator", + "token_id" + ], + "properties": { + "operator": { + "$ref": "#/definitions/HumanAddr" + }, + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + } + ], + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", + "type": "string" + }, + "Expiration": { + "anyOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + } + } + ] + }, + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/cw721_query_msg.json b/packages/cw721/schema/cw721_query_msg.json new file mode 100644 index 000000000..74d875423 --- /dev/null +++ b/packages/cw721/schema/cw721_query_msg.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721QueryMsg", + "anyOf": [ + { + "description": "Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + }, + { + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "required": [ + "approved_for_all" + ], + "properties": { + "approved_for_all": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + } + ], + "definitions": { + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json new file mode 100644 index 000000000..1ed33576a --- /dev/null +++ b/packages/cw721/schema/owner_of_response.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "approval": { + "anyOf": [ + { + "$ref": "#/definitions/Approval" + }, + { + "type": "null" + } + ] + }, + "owner": { + "$ref": "#/definitions/HumanAddr" + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "approved", + "expires" + ], + "properties": { + "approved": { + "$ref": "#/definitions/HumanAddr" + }, + "expires": { + "$ref": "#/definitions/Expiration" + } + } + }, + "Expiration": { + "anyOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + } + } + ] + }, + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/tokens_response.json b/packages/cw721/schema/tokens_response.json new file mode 100644 index 000000000..1ef6ca312 --- /dev/null +++ b/packages/cw721/schema/tokens_response.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs new file mode 100644 index 000000000..497676605 --- /dev/null +++ b/packages/cw721/src/lib.rs @@ -0,0 +1,15 @@ +mod msg; +mod query; + +// pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; +pub use crate::msg::{Cw721HandleMsg, Expiration}; +pub use crate::query::{ApprovedForAllResponse, Cw721QueryMsg, OwnerOfResponse, TokensResponse}; +// pub use crate::receiver::Cw20ReceiveMsg; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/packages/cw721/src/msg.rs b/packages/cw721/src/msg.rs new file mode 100644 index 000000000..a0b27eba5 --- /dev/null +++ b/packages/cw721/src/msg.rs @@ -0,0 +1,69 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Binary, BlockInfo, HumanAddr}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Cw721HandleMsg { + /// Transfer is a base message to move a token to another account without triggering actions + TransferNft { + recipient: HumanAddr, + token_id: String, + }, + /// Send is a base message to transfer a token to a contract and trigger an action + /// on the receiving contract. + SendNft { + contract: HumanAddr, + token_id: String, + msg: Option, + }, + /// Allows operator to transfer / send the token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + Approve { + operator: HumanAddr, + token_id: String, + expires: Option, + }, + /// Remove previously granted Approval + Revoke { + operator: HumanAddr, + token_id: String, + }, + /// Allows operator to transfer / send any token from the owner's account. + /// If expiration is set, then this allowance has a time/height limit + ApproveAll { + operator: HumanAddr, + expires: Option, + }, + /// Remove previously granted ApproveAll permission + RevokeAll { operator: HumanAddr }, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Expiration { + /// AtHeight will expire when `env.block.height` >= height + AtHeight(u64), + /// AtTime will expire when `env.block.time` >= time + AtTime(u64), + /// Never will never expire. Used to distinguish None from Some(Expiration::Never) + Never {}, +} + +/// The default (empty value) is to never expire +impl Default for Expiration { + fn default() -> Self { + Expiration::Never {} + } +} + +impl Expiration { + pub fn is_expired(&self, block: &BlockInfo) -> bool { + match self { + Expiration::AtHeight(height) => block.height >= *height, + Expiration::AtTime(time) => block.time >= *time, + Expiration::Never {} => false, + } + } +} diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs new file mode 100644 index 000000000..8d168af2b --- /dev/null +++ b/packages/cw721/src/query.rs @@ -0,0 +1,48 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::HumanAddr; + +use crate::msg::Expiration; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Cw721QueryMsg { + /// Returns all tokens owned by the given address, [] if unset. + /// Return type: TokensResponse. + Tokens { + owner: HumanAddr, + }, + // Return the owner of the given token, error if token does not exist + // Return type: OwnerOfResponse + OwnerOf { + token_id: String, + }, + // List all operators that can access all of the owner's tokens + // Return type: `ApprovedForAllResponse` + ApprovedForAll { + owner: HumanAddr, + }, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct TokensResponse { + pub tokens: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct OwnerOfResponse { + pub owner: HumanAddr, + pub approval: Option, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Approval { + pub approved: HumanAddr, + pub expires: Expiration, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ApprovedForAllResponse { + pub operators: Vec, +} From 981f8a4a984e60b60213611c311dfc5de8a02126 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 20 Aug 2020 23:39:28 +0200 Subject: [PATCH 03/10] Add metadata and enumeration extensions --- packages/cw721/README.md | 53 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/cw721/README.md b/packages/cw721/README.md index c428b55dc..16dd0b177 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -58,12 +58,6 @@ as well as anyone with approval on this particular token. If the token is unknown, returns an error. Return type is `OwnerResponse{owner}`. -**TODO** Do we want to be this ambitious? Handle paging? -Right now, let it just get big and expensive to query - -`Tokens{owner}` - List all token_ids that belong to a given owner. -Return type is `TokensResponse{tokens: Vec}`. - `ApprovedForAll{owner}` - List all operators that can access all of the owner's tokens. Return type is `ApprovedForAllResponse` @@ -85,11 +79,50 @@ or it may be a `ReceiveMsg` variant to clarify the intention. For example, if I send to an exchange, I can specify the price I want to list the token for. -## Mintable +## Metadata -Can we generalize this??? +### Queries -### Query With Data +`NftInfo{}` - This returns top-level metadata about the contract. +Namely, `name` and `symbol`. + +`TokenUri{token_id}` - Returns a URI that represents this token. +URIs are defined in RFC 3986. The URI may point either to an image +file or to a JSON file that conforms to the "CW721 Metadata JSON Schema". + +An "image file" has mime type image/* and 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. + +*CW721 Metadata JSON Schema* is currently a copy of ERC721: + +```json +{ + "title": "Asset Metadata", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Identifies the asset to which this NFT represents" + }, + "description": { + "type": "string", + "description": "Describes the asset to which this NFT represents" + }, + "image": { + "type": "string", + "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." + } + } +} +``` + +## Enumerable -Can we generalize the data types??? +### Queries + +**TODO** Handle paging? Right now, let it just get big and +expensive to query. This API will most likely evolve. +`Tokens{owner}` - List all token_ids that belong to a given owner. +Return type is `TokensResponse{tokens: Vec}`. From 2d07697ec3024e1bf4eccf5e22954ed8d9d2286d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 21 Aug 2020 09:46:30 +0200 Subject: [PATCH 04/10] Update extensions, implement with types --- packages/cw721/README.md | 44 ++----- packages/cw721/examples/schema.rs | 7 +- .../cw721/schema/all_nft_info_response.json | 119 ++++++++++++++++++ .../cw721/schema/contract_info_response.json | 17 +++ packages/cw721/schema/cw721_query_msg.json | 92 ++++++++++++-- packages/cw721/schema/nft_info_response.json | 24 ++++ .../cw721/schema/num_tokens_response.json | 15 +++ packages/cw721/schema/owner_of_response.json | 22 +++- packages/cw721/src/lib.rs | 5 +- packages/cw721/src/query.rs | 89 +++++++++++-- 10 files changed, 378 insertions(+), 56 deletions(-) create mode 100644 packages/cw721/schema/all_nft_info_response.json create mode 100644 packages/cw721/schema/contract_info_response.json create mode 100644 packages/cw721/schema/nft_info_response.json create mode 100644 packages/cw721/schema/num_tokens_response.json diff --git a/packages/cw721/README.md b/packages/cw721/README.md index 16dd0b177..ab8b0be69 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -61,6 +61,8 @@ If the token is unknown, returns an error. Return type is `ApprovedForAll{owner}` - List all operators that can access all of the owner's tokens. Return type is `ApprovedForAllResponse` +`NumTokens{}` - Total number of tokens issued + ### Receiver The counter-part to `SendNft` is `ReceiveNft`, which must be implemented by @@ -83,39 +85,16 @@ for. ### Queries -`NftInfo{}` - This returns top-level metadata about the contract. +`ContractInfo{}` - This returns top-level metadata about the contract. Namely, `name` and `symbol`. -`TokenUri{token_id}` - Returns a URI that represents this token. -URIs are defined in RFC 3986. The URI may point either to an image -file or to a JSON file that conforms to the "CW721 Metadata JSON Schema". - -An "image file" has mime type image/* and 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. - -*CW721 Metadata JSON Schema* is currently a copy of ERC721: - -```json -{ - "title": "Asset Metadata", - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Identifies the asset to which this NFT represents" - }, - "description": { - "type": "string", - "description": "Describes the asset to which this NFT represents" - }, - "image": { - "type": "string", - "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." - } - } -} -``` +`NftInfo{token_id}` - This returns metadata about one particular token. +The return value is based on *ERC721 Metadata JSON Schema*, but directly +from the contract, not as a Uri. Only the image link is a Uri. + +`AllNftInfo{token_id}` - This returns the result of both `NftInfo` +and `OwnerOf` as one query as an optimization for clients, which may +want both info to display one NFT. ## Enumerable @@ -126,3 +105,6 @@ expensive to query. This API will most likely evolve. `Tokens{owner}` - List all token_ids that belong to a given owner. Return type is `TokensResponse{tokens: Vec}`. + +`AllTokens{}` - Requires pagination. Lists all token_ids controlled by +the contract. diff --git a/packages/cw721/examples/schema.rs b/packages/cw721/examples/schema.rs index 76fac8272..dd7f2a059 100644 --- a/packages/cw721/examples/schema.rs +++ b/packages/cw721/examples/schema.rs @@ -4,7 +4,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use cw721::{ - ApprovedForAllResponse, Cw721HandleMsg, Cw721QueryMsg, OwnerOfResponse, TokensResponse, + AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721HandleMsg, + Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, }; fn main() { @@ -16,7 +17,11 @@ fn main() { export_schema(&schema_for!(Cw721HandleMsg), &out_dir); export_schema(&schema_for!(Cw721QueryMsg), &out_dir); // export_schema(&schema_for!(Cw20ReceiveMsg), &out_dir); + export_schema(&schema_for!(AllNftInfoResponse), &out_dir); export_schema(&schema_for!(ApprovedForAllResponse), &out_dir); + export_schema(&schema_for!(ContractInfoResponse), &out_dir); export_schema(&schema_for!(OwnerOfResponse), &out_dir); + export_schema(&schema_for!(NftInfoResponse), &out_dir); + export_schema(&schema_for!(NumTokensResponse), &out_dir); export_schema(&schema_for!(TokensResponse), &out_dir); } diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json new file mode 100644 index 000000000..674d2133e --- /dev/null +++ b/packages/cw721/schema/all_nft_info_response.json @@ -0,0 +1,119 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse", + "description": "TODO: cleaner way of combining the two structs than cut and paste?", + "type": "object", + "required": [ + "description", + "image", + "name", + "owner" + ], + "properties": { + "approval": { + "description": "If set this address is approved to transfer/send the token as well", + "anyOf": [ + { + "$ref": "#/definitions/Approval" + }, + { + "type": "null" + } + ] + }, + "description": { + "description": "Describes the asset to which this NFT represents", + "type": "string" + }, + "image": { + "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: Custom URI type??", + "type": "string" + }, + "name": { + "description": "Identifies the asset to which this NFT represents", + "type": "string" + }, + "owner": { + "description": "Owner of the token", + "allOf": [ + { + "$ref": "#/definitions/HumanAddr" + } + ] + } + }, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "approved", + "expires" + ], + "properties": { + "approved": { + "description": "Account that can transfer/send the token", + "allOf": [ + { + "$ref": "#/definitions/HumanAddr" + } + ] + }, + "expires": { + "description": "When the Approval expires (maybe never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + } + } + }, + "Expiration": { + "anyOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + } + } + ] + }, + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/contract_info_response.json b/packages/cw721/schema/contract_info_response.json new file mode 100644 index 000000000..a16712589 --- /dev/null +++ b/packages/cw721/schema/contract_info_response.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/cw721_query_msg.json b/packages/cw721/schema/cw721_query_msg.json index 74d875423..fb965c51d 100644 --- a/packages/cw721/schema/cw721_query_msg.json +++ b/packages/cw721/schema/cw721_query_msg.json @@ -3,13 +3,33 @@ "title": "Cw721QueryMsg", "anyOf": [ { - "description": "Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "description": "Return the owner of the given token, error if token does not exist Return type: OwnerOfResponse", "type": "object", "required": [ - "tokens" + "owner_of" ], "properties": { - "tokens": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "List all operators that can access all of the owner's tokens Return type: `ApprovedForAllResponse`", + "type": "object", + "required": [ + "approved_for_all" + ], + "properties": { + "approved_for_all": { "type": "object", "required": [ "owner" @@ -23,12 +43,37 @@ } }, { + "description": "Total number of tokens issued", "type": "object", "required": [ - "owner_of" + "num_tokens" ], "properties": { - "owner_of": { + "num_tokens": { + "type": "object" + } + } + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract: `ContractInfoResponse`", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object" + } + } + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract: `NftInfoResponse`", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { "type": "object", "required": [ "token_id" @@ -42,12 +87,33 @@ } }, { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients: `AllNftInfo`", "type": "object", "required": [ - "approved_for_all" + "all_nft_info" ], "properties": { - "approved_for_all": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + } + } + } + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset. Return type: TokensResponse.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { "type": "object", "required": [ "owner" @@ -59,6 +125,18 @@ } } } + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object" + } + } } ], "definitions": { diff --git a/packages/cw721/schema/nft_info_response.json b/packages/cw721/schema/nft_info_response.json new file mode 100644 index 000000000..5fa3af3be --- /dev/null +++ b/packages/cw721/schema/nft_info_response.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse", + "type": "object", + "required": [ + "description", + "image", + "name" + ], + "properties": { + "description": { + "description": "Describes the asset to which this NFT represents", + "type": "string" + }, + "image": { + "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: Custom URI type??", + "type": "string" + }, + "name": { + "description": "Identifies the asset to which this NFT represents", + "type": "string" + } + } +} diff --git a/packages/cw721/schema/num_tokens_response.json b/packages/cw721/schema/num_tokens_response.json new file mode 100644 index 000000000..4647c23aa --- /dev/null +++ b/packages/cw721/schema/num_tokens_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json index 1ed33576a..30de91035 100644 --- a/packages/cw721/schema/owner_of_response.json +++ b/packages/cw721/schema/owner_of_response.json @@ -7,6 +7,7 @@ ], "properties": { "approval": { + "description": "If set this address is approved to transfer/send the token as well", "anyOf": [ { "$ref": "#/definitions/Approval" @@ -17,7 +18,12 @@ ] }, "owner": { - "$ref": "#/definitions/HumanAddr" + "description": "Owner of the token", + "allOf": [ + { + "$ref": "#/definitions/HumanAddr" + } + ] } }, "definitions": { @@ -29,10 +35,20 @@ ], "properties": { "approved": { - "$ref": "#/definitions/HumanAddr" + "description": "Account that can transfer/send the token", + "allOf": [ + { + "$ref": "#/definitions/HumanAddr" + } + ] }, "expires": { - "$ref": "#/definitions/Expiration" + "description": "When the Approval expires (maybe never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] } } }, diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 497676605..d2aeebcb8 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -3,7 +3,10 @@ mod query; // pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; pub use crate::msg::{Cw721HandleMsg, Expiration}; -pub use crate::query::{ApprovedForAllResponse, Cw721QueryMsg, OwnerOfResponse, TokensResponse}; +pub use crate::query::{ + AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721QueryMsg, + NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, +}; // pub use crate::receiver::Cw20ReceiveMsg; #[cfg(test)] diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs index 8d168af2b..95829ace5 100644 --- a/packages/cw721/src/query.rs +++ b/packages/cw721/src/query.rs @@ -8,21 +8,35 @@ use crate::msg::Expiration; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum Cw721QueryMsg { + /// Return the owner of the given token, error if token does not exist + /// Return type: OwnerOfResponse + OwnerOf { token_id: String }, + /// List all operators that can access all of the owner's tokens + /// Return type: `ApprovedForAllResponse` + ApprovedForAll { owner: HumanAddr }, + /// Total number of tokens issued + NumTokens {}, + + /// With MetaData Extension. + /// Returns top-level metadata about the contract: `ContractInfoResponse` + ContractInfo {}, + /// With MetaData Extension. + /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* + /// but directly from the contract: `NftInfoResponse` + NftInfo { token_id: String }, + /// With MetaData Extension. + /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization + /// for clients: `AllNftInfo` + AllNftInfo { token_id: String }, + + /// With Enumerable extension. /// Returns all tokens owned by the given address, [] if unset. /// Return type: TokensResponse. - Tokens { - owner: HumanAddr, - }, - // Return the owner of the given token, error if token does not exist - // Return type: OwnerOfResponse - OwnerOf { - token_id: String, - }, - // List all operators that can access all of the owner's tokens - // Return type: `ApprovedForAllResponse` - ApprovedForAll { - owner: HumanAddr, - }, + Tokens { owner: HumanAddr }, + /// With Enumerable extension. + /// Requires pagination. Lists all token_ids controlled by the contract. + /// Return type: TokensResponse. + AllTokens {}, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -32,13 +46,17 @@ pub struct TokensResponse { #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct OwnerOfResponse { + /// Owner of the token pub owner: HumanAddr, + /// If set this address is approved to transfer/send the token as well pub approval: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct Approval { + /// Account that can transfer/send the token pub approved: HumanAddr, + /// When the Approval expires (maybe never) pub expires: Expiration, } @@ -46,3 +64,48 @@ pub struct Approval { pub struct ApprovedForAllResponse { pub operators: Vec, } + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct NumTokensResponse { + pub count: u64, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct ContractInfoResponse { + pub name: String, + pub symbol: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct NftInfoResponse { + /// Identifies the asset to which this NFT represents + pub name: String, + /// Describes the asset to which this NFT represents + pub description: String, + /// "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: Custom URI type?? + pub image: String, + // TODO: make this extensible with extra fields? (Response must be a superset of this) +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +/// TODO: cleaner way of combining the two structs than cut and paste? +pub struct AllNftInfoResponse { + /// Owner of the token + pub owner: HumanAddr, + /// If set this address is approved to transfer/send the token as well + pub approval: Option, + + /// Identifies the asset to which this NFT represents + pub name: String, + /// Describes the asset to which this NFT represents + pub description: String, + /// "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: Custom URI type?? + pub image: String, + // TODO: make this extensible with extra fields? (Response must be a superset of this) +} From fe20d66aa5188245ae79cb12dd60d740468d64eb Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 21 Aug 2020 10:03:47 +0200 Subject: [PATCH 05/10] Add Cw721RecieveMsg --- packages/cw721/examples/schema.rs | 5 ++- .../cw721/schema/all_nft_info_response.json | 2 +- packages/cw721/schema/cw721_handle_msg.json | 2 +- packages/cw721/schema/cw721_receive_msg.json | 40 +++++++++++++++++++ packages/cw721/schema/owner_of_response.json | 2 +- packages/cw721/src/lib.rs | 3 +- packages/cw721/src/msg.rs | 2 +- packages/cw721/src/receiver.rs | 40 +++++++++++++++++++ 8 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 packages/cw721/schema/cw721_receive_msg.json create mode 100644 packages/cw721/src/receiver.rs diff --git a/packages/cw721/examples/schema.rs b/packages/cw721/examples/schema.rs index dd7f2a059..4f694ddde 100644 --- a/packages/cw721/examples/schema.rs +++ b/packages/cw721/examples/schema.rs @@ -5,7 +5,8 @@ use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use cw721::{ AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721HandleMsg, - Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, + Cw721QueryMsg, Cw721ReceiveMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, + TokensResponse, }; fn main() { @@ -16,7 +17,7 @@ fn main() { export_schema(&schema_for!(Cw721HandleMsg), &out_dir); export_schema(&schema_for!(Cw721QueryMsg), &out_dir); - // export_schema(&schema_for!(Cw20ReceiveMsg), &out_dir); + export_schema(&schema_for!(Cw721ReceiveMsg), &out_dir); export_schema(&schema_for!(AllNftInfoResponse), &out_dir); export_schema(&schema_for!(ApprovedForAllResponse), &out_dir); export_schema(&schema_for!(ContractInfoResponse), &out_dir); diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json index 674d2133e..2583c9ee6 100644 --- a/packages/cw721/schema/all_nft_info_response.json +++ b/packages/cw721/schema/all_nft_info_response.json @@ -99,7 +99,7 @@ } }, { - "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "description": "Never will never expire. Used to express the empty variant", "type": "object", "required": [ "never" diff --git a/packages/cw721/schema/cw721_handle_msg.json b/packages/cw721/schema/cw721_handle_msg.json index 858db43dc..741f19bda 100644 --- a/packages/cw721/schema/cw721_handle_msg.json +++ b/packages/cw721/schema/cw721_handle_msg.json @@ -205,7 +205,7 @@ } }, { - "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "description": "Never will never expire. Used to express the empty variant", "type": "object", "required": [ "never" diff --git a/packages/cw721/schema/cw721_receive_msg.json b/packages/cw721/schema/cw721_receive_msg.json new file mode 100644 index 000000000..78f14a9c2 --- /dev/null +++ b/packages/cw721/schema/cw721_receive_msg.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cw721ReceiveMsg", + "description": "Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg", + "type": "object", + "required": [ + "amount", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "anyOf": [ + { + "$ref": "#/definitions/Binary" + }, + { + "type": "null" + } + ] + }, + "sender": { + "$ref": "#/definitions/HumanAddr" + } + }, + "definitions": { + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec", + "type": "string" + }, + "HumanAddr": { + "type": "string" + }, + "Uint128": { + "type": "string" + } + } +} diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json index 30de91035..8e8bb5550 100644 --- a/packages/cw721/schema/owner_of_response.json +++ b/packages/cw721/schema/owner_of_response.json @@ -83,7 +83,7 @@ } }, { - "description": "Never will never expire. Used to distinguish None from Some(Expiration::Never)", + "description": "Never will never expire. Used to express the empty variant", "type": "object", "required": [ "never" diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index d2aeebcb8..13c43b14c 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,5 +1,6 @@ mod msg; mod query; +mod receiver; // pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; pub use crate::msg::{Cw721HandleMsg, Expiration}; @@ -7,7 +8,7 @@ pub use crate::query::{ AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, }; -// pub use crate::receiver::Cw20ReceiveMsg; +pub use crate::receiver::Cw721ReceiveMsg; #[cfg(test)] mod tests { diff --git a/packages/cw721/src/msg.rs b/packages/cw721/src/msg.rs index a0b27eba5..f6bf7ffd1 100644 --- a/packages/cw721/src/msg.rs +++ b/packages/cw721/src/msg.rs @@ -47,7 +47,7 @@ pub enum Expiration { AtHeight(u64), /// AtTime will expire when `env.block.time` >= time AtTime(u64), - /// Never will never expire. Used to distinguish None from Some(Expiration::Never) + /// Never will never expire. Used to express the empty variant Never {}, } diff --git a/packages/cw721/src/receiver.rs b/packages/cw721/src/receiver.rs new file mode 100644 index 000000000..02c189916 --- /dev/null +++ b/packages/cw721/src/receiver.rs @@ -0,0 +1,40 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{to_binary, Binary, CosmosMsg, HumanAddr, StdResult, Uint128, WasmMsg}; + +/// Cw721ReceiveMsg should be de/serialized under `Receive()` variant in a HandleMsg +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct Cw721ReceiveMsg { + pub sender: HumanAddr, + pub amount: Uint128, + pub msg: Option, +} + +impl Cw721ReceiveMsg { + /// serializes the message + pub fn into_binary(self) -> StdResult { + let msg = ReceiverHandleMsg::ReceiveNft(self); + to_binary(&msg) + } + + /// creates a cosmos_msg sending this struct to the named contract + pub fn into_cosmos_msg(self, contract_addr: HumanAddr) -> StdResult { + let msg = self.into_binary()?; + let execute = WasmMsg::Execute { + contract_addr, + msg, + send: vec![], + }; + Ok(execute.into()) + } +} + +/// This is just a helper to properly serialize the above message. +/// The actual receiver should include this variant in the larger HandleMsg enum +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +enum ReceiverHandleMsg { + ReceiveNft(Cw721ReceiveMsg), +} From 41cde2c29d923a382bcb41f220f0bfffa664c5d4 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 21 Aug 2020 21:51:40 +0200 Subject: [PATCH 06/10] Clean up query responses --- .../cw721/schema/all_nft_info_response.json | 86 +++++++++++++------ packages/cw721/schema/nft_info_response.json | 8 +- packages/cw721/src/query.rs | 24 ++---- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json index 2583c9ee6..19476f23a 100644 --- a/packages/cw721/schema/all_nft_info_response.json +++ b/packages/cw721/schema/all_nft_info_response.json @@ -4,40 +4,23 @@ "description": "TODO: cleaner way of combining the two structs than cut and paste?", "type": "object", "required": [ - "description", - "image", - "name", - "owner" + "access", + "info" ], "properties": { - "approval": { - "description": "If set this address is approved to transfer/send the token as well", - "anyOf": [ - { - "$ref": "#/definitions/Approval" - }, + "access": { + "description": "Who can transfer the token", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/OwnerOfResponse" } ] }, - "description": { - "description": "Describes the asset to which this NFT represents", - "type": "string" - }, - "image": { - "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: Custom URI type??", - "type": "string" - }, - "name": { - "description": "Identifies the asset to which this NFT represents", - "type": "string" - }, - "owner": { - "description": "Owner of the token", + "info": { + "description": "Data on the token itself,", "allOf": [ { - "$ref": "#/definitions/HumanAddr" + "$ref": "#/definitions/NftInfoResponse" } ] } @@ -114,6 +97,57 @@ }, "HumanAddr": { "type": "string" + }, + "NftInfoResponse": { + "type": "object", + "required": [ + "description", + "name" + ], + "properties": { + "description": { + "description": "Describes the asset to which this NFT represents", + "type": "string" + }, + "image": { + "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", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Identifies the asset to which this NFT represents", + "type": "string" + } + } + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "approval": { + "description": "If set this address is approved to transfer/send the token as well", + "anyOf": [ + { + "$ref": "#/definitions/Approval" + }, + { + "type": "null" + } + ] + }, + "owner": { + "description": "Owner of the token", + "allOf": [ + { + "$ref": "#/definitions/HumanAddr" + } + ] + } + } } } } diff --git a/packages/cw721/schema/nft_info_response.json b/packages/cw721/schema/nft_info_response.json index 5fa3af3be..6adc201e4 100644 --- a/packages/cw721/schema/nft_info_response.json +++ b/packages/cw721/schema/nft_info_response.json @@ -4,7 +4,6 @@ "type": "object", "required": [ "description", - "image", "name" ], "properties": { @@ -13,8 +12,11 @@ "type": "string" }, "image": { - "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: Custom URI type??", - "type": "string" + "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", + "type": [ + "string", + "null" + ] }, "name": { "description": "Identifies the asset to which this NFT represents", diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs index 95829ace5..81e1cf04f 100644 --- a/packages/cw721/src/query.rs +++ b/packages/cw721/src/query.rs @@ -85,27 +85,15 @@ pub struct NftInfoResponse { /// "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: Custom URI type?? - pub image: String, - // TODO: make this extensible with extra fields? (Response must be a superset of this) + /// TODO: Use https://docs.rs/url_serde for type-safety + pub image: Option, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] /// TODO: cleaner way of combining the two structs than cut and paste? pub struct AllNftInfoResponse { - /// Owner of the token - pub owner: HumanAddr, - /// If set this address is approved to transfer/send the token as well - pub approval: Option, - - /// Identifies the asset to which this NFT represents - pub name: String, - /// Describes the asset to which this NFT represents - pub description: String, - /// "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: Custom URI type?? - pub image: String, - // TODO: make this extensible with extra fields? (Response must be a superset of this) + /// Who can transfer the token + pub access: OwnerOfResponse, + /// Data on the token itself, + pub info: NftInfoResponse, } From d8e00498016bc38b2208abc6b08cf433a1a51add Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 21 Aug 2020 22:10:07 +0200 Subject: [PATCH 07/10] Add query helpers --- packages/cw721/src/helpers.rs | 153 ++++++++++++++++++++++++++++++++++ packages/cw721/src/lib.rs | 3 +- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 packages/cw721/src/helpers.rs diff --git a/packages/cw721/src/helpers.rs b/packages/cw721/src/helpers.rs new file mode 100644 index 000000000..c0e934e4b --- /dev/null +++ b/packages/cw721/src/helpers.rs @@ -0,0 +1,153 @@ +use schemars::JsonSchema; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +use cosmwasm_std::{ + to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, Querier, StdResult, WasmMsg, WasmQuery, +}; + +use crate::{ + AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721HandleMsg, + Cw721QueryMsg, NftInfoResponse, NumTokensResponse, OwnerOfResponse, TokensResponse, +}; + +/// Cw721Contract is a wrapper around HumanAddr that provides a lot of helpers +/// for working with this. +/// +/// If you wish to persist this, convert to Cw721CanonicalContract via .canonical() +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Cw721Contract(pub HumanAddr); + +impl Cw721Contract { + pub fn addr(&self) -> HumanAddr { + self.0.clone() + } + + /// Convert this address to a form fit for storage + pub fn canonical(&self, api: &A) -> StdResult { + let canon = api.canonical_address(&self.0)?; + Ok(Cw721CanonicalContract(canon)) + } + + pub fn call(&self, msg: Cw721HandleMsg) -> StdResult { + let msg = to_binary(&msg)?; + Ok(WasmMsg::Execute { + contract_addr: self.addr(), + msg, + send: vec![], + } + .into()) + } + + pub fn query( + &self, + querier: &Q, + req: Cw721QueryMsg, + ) -> StdResult { + let query = WasmQuery::Smart { + contract_addr: self.addr(), + msg: to_binary(&req)?, + } + .into(); + querier.query(&query) + } + + pub fn owner_of>( + &self, + querier: &Q, + token_id: T, + ) -> StdResult { + let req = Cw721QueryMsg::OwnerOf { + token_id: token_id.into(), + }; + self.query(querier, req) + } + + pub fn approved_for_all>( + &self, + querier: &Q, + owner: T, + ) -> StdResult> { + let req = Cw721QueryMsg::ApprovedForAll { + owner: owner.into(), + }; + let res: ApprovedForAllResponse = self.query(querier, req)?; + Ok(res.operators) + } + + pub fn num_tokens(&self, querier: &Q) -> StdResult { + let req = Cw721QueryMsg::NumTokens {}; + let res: NumTokensResponse = self.query(querier, req)?; + Ok(res.count) + } + + /// With metadata extension + pub fn contract_info(&self, querier: &Q) -> StdResult { + let req = Cw721QueryMsg::ContractInfo {}; + self.query(querier, req) + } + + /// With metadata extension + pub fn nft_info>( + &self, + querier: &Q, + token_id: T, + ) -> StdResult { + let req = Cw721QueryMsg::NftInfo { + token_id: token_id.into(), + }; + self.query(querier, req) + } + + /// With metadata extension + pub fn all_nft_info>( + &self, + querier: &Q, + token_id: T, + ) -> StdResult { + let req = Cw721QueryMsg::AllNftInfo { + token_id: token_id.into(), + }; + self.query(querier, req) + } + + /// With enumerable extension + pub fn tokens>( + &self, + querier: &Q, + owner: T, + ) -> StdResult { + let req = Cw721QueryMsg::Tokens { + owner: owner.into(), + }; + self.query(querier, req) + } + + /// With enumerable extension + pub fn all_tokens(&self, querier: &Q) -> StdResult { + let req = Cw721QueryMsg::AllTokens {}; + self.query(querier, req) + } + + /// returns true if the contract supports the metadata extension + pub fn has_metadata(&self, querier: &Q) -> bool { + self.contract_info(querier).is_ok() + } + + /// returns true if the contract supports the enumerable extension + pub fn has_enumerable(&self, querier: &Q) -> bool { + self.tokens(querier, self.addr()).is_ok() + } +} + +/// This is a respresentation of Cw721Contract for storage. +/// Don't use it directly, just translate to the Cw721Contract when needed. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Cw721CanonicalContract(pub CanonicalAddr); + +impl Cw721CanonicalContract { + /// Convert this address to a form fit for usage in messages and queries + pub fn human(&self, api: &A) -> StdResult { + let human = api.human_address(&self.0)?; + Ok(Cw721Contract(human)) + } +} diff --git a/packages/cw721/src/lib.rs b/packages/cw721/src/lib.rs index 13c43b14c..c5bf5c6ae 100644 --- a/packages/cw721/src/lib.rs +++ b/packages/cw721/src/lib.rs @@ -1,8 +1,9 @@ +mod helpers; mod msg; mod query; mod receiver; -// pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; +pub use crate::helpers::{Cw721CanonicalContract, Cw721Contract}; pub use crate::msg::{Cw721HandleMsg, Expiration}; pub use crate::query::{ AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Cw721QueryMsg, From 4f27016aebd4b5555cff9febb81eb812c0e481f9 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 21 Aug 2020 23:00:33 +0200 Subject: [PATCH 08/10] Add cw721 to CI --- .circleci/config.yml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4447f3b2a..a8d7b3585 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,7 @@ workflows: - package_cw1 - package_cw2 - package_cw20 + - package_cw721 - lint deploy: jobs: @@ -303,6 +304,49 @@ jobs: - target key: cargocache-v2-cw20:1.44.1-{{ checksum "~/project/Cargo.lock" }} + package_cw721: + docker: + - image: rust:1.44.1 + working_directory: ~/project/packages/cw721 + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version; rustup target list --installed + - restore_cache: + keys: + - cargocache-v2-cw721:1.44.1-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Add wasm32 target + command: rustup target add wasm32-unknown-unknown && rustup target list --installed + - run: + name: Build library for native target + command: cargo build --locked + - run: + name: Build library for wasm target + command: cargo wasm --locked + - run: + name: Run unit tests + command: cargo test --locked + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-v2-cw721:1.44.1-{{ checksum "~/project/Cargo.lock" }} + lint: docker: - image: rust:1.44.1 From b79dab6d4423023212632c6fd272773836990b40 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 24 Aug 2020 22:57:47 +0200 Subject: [PATCH 09/10] Allow N approvals for one NFT, fix some wording --- packages/cw721/README.md | 12 +++---- .../cw721/schema/all_nft_info_response.json | 31 +++++++++---------- packages/cw721/schema/cw721_handle_msg.json | 8 ++--- packages/cw721/schema/owner_of_response.json | 31 +++++++++---------- packages/cw721/src/msg.rs | 4 +-- packages/cw721/src/query.rs | 6 ++-- 6 files changed, 43 insertions(+), 49 deletions(-) diff --git a/packages/cw721/README.md b/packages/cw721/README.md index ab8b0be69..8d83b9459 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -1,6 +1,6 @@ # CW721 Spec: Non Fungible Tokens -CW721 is a specification for fungible tokens based on CosmWasm. +CW721 is a specification for non-fungible tokens based on CosmWasm. The name and design is based on Ethereum's ERC721 standard, with some enhancements. The types in here can be imported by contracts that wish to implement this spec, or by contracts that call @@ -34,18 +34,18 @@ contract, along with the token_id. Requires `token_id` to point to a valid token, and `env.sender` to be the owner of it, or have an allowance to transfer it. -`Approve{approved, token_id, expires}` - Grants permission to `approved` to +`Approve{spender, token_id, expires}` - Grants permission to `spender` to transfer or send the given token. This can only be performed when `env.sender` is the owner of the given `token_id` or an `operator`. -There can only be one approved account per token, and it is cleared once -the token is transfered or sent. +There can multiple spender accounts per token, and they are cleared once +the token is transfered or sent. -`Revoke{token_id}` - This revokes a previously granted permission +`Revoke{spender, token_id}` - This revokes a previously granted permission to transfer the given `token_id`. This can only be granted when `env.sender` is the owner of the given `token_id` or an `operator`. `ApproveAll{operator, expires}` - Grant `operator` permission to transfer or send -all tokens owner by `env.sender`. This is tied to the owner, not the +all tokens owned by `env.sender`. This approval is tied to the owner, not the tokens and applies to any future token that the owner receives as well. `RevokeAll{operator}` - Revoke a previous `ApproveAll` permission granted diff --git a/packages/cw721/schema/all_nft_info_response.json b/packages/cw721/schema/all_nft_info_response.json index 19476f23a..657a60a09 100644 --- a/packages/cw721/schema/all_nft_info_response.json +++ b/packages/cw721/schema/all_nft_info_response.json @@ -29,23 +29,23 @@ "Approval": { "type": "object", "required": [ - "approved", - "expires" + "expires", + "spender" ], "properties": { - "approved": { - "description": "Account that can transfer/send the token", + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", "allOf": [ { - "$ref": "#/definitions/HumanAddr" + "$ref": "#/definitions/Expiration" } ] }, - "expires": { - "description": "When the Approval expires (maybe never)", + "spender": { + "description": "Account that can transfer/send the token", "allOf": [ { - "$ref": "#/definitions/Expiration" + "$ref": "#/definitions/HumanAddr" } ] } @@ -125,19 +125,16 @@ "OwnerOfResponse": { "type": "object", "required": [ + "approvals", "owner" ], "properties": { - "approval": { + "approvals": { "description": "If set this address is approved to transfer/send the token as well", - "anyOf": [ - { - "$ref": "#/definitions/Approval" - }, - { - "type": "null" - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } }, "owner": { "description": "Owner of the token", diff --git a/packages/cw721/schema/cw721_handle_msg.json b/packages/cw721/schema/cw721_handle_msg.json index 741f19bda..36557ce5b 100644 --- a/packages/cw721/schema/cw721_handle_msg.json +++ b/packages/cw721/schema/cw721_handle_msg.json @@ -70,7 +70,7 @@ "approve": { "type": "object", "required": [ - "operator", + "spender", "token_id" ], "properties": { @@ -84,7 +84,7 @@ } ] }, - "operator": { + "spender": { "$ref": "#/definitions/HumanAddr" }, "token_id": { @@ -104,11 +104,11 @@ "revoke": { "type": "object", "required": [ - "operator", + "spender", "token_id" ], "properties": { - "operator": { + "spender": { "$ref": "#/definitions/HumanAddr" }, "token_id": { diff --git a/packages/cw721/schema/owner_of_response.json b/packages/cw721/schema/owner_of_response.json index 8e8bb5550..56d57d55c 100644 --- a/packages/cw721/schema/owner_of_response.json +++ b/packages/cw721/schema/owner_of_response.json @@ -3,19 +3,16 @@ "title": "OwnerOfResponse", "type": "object", "required": [ + "approvals", "owner" ], "properties": { - "approval": { + "approvals": { "description": "If set this address is approved to transfer/send the token as well", - "anyOf": [ - { - "$ref": "#/definitions/Approval" - }, - { - "type": "null" - } - ] + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } }, "owner": { "description": "Owner of the token", @@ -30,23 +27,23 @@ "Approval": { "type": "object", "required": [ - "approved", - "expires" + "expires", + "spender" ], "properties": { - "approved": { - "description": "Account that can transfer/send the token", + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", "allOf": [ { - "$ref": "#/definitions/HumanAddr" + "$ref": "#/definitions/Expiration" } ] }, - "expires": { - "description": "When the Approval expires (maybe never)", + "spender": { + "description": "Account that can transfer/send the token", "allOf": [ { - "$ref": "#/definitions/Expiration" + "$ref": "#/definitions/HumanAddr" } ] } diff --git a/packages/cw721/src/msg.rs b/packages/cw721/src/msg.rs index f6bf7ffd1..6a5bf0585 100644 --- a/packages/cw721/src/msg.rs +++ b/packages/cw721/src/msg.rs @@ -21,13 +21,13 @@ pub enum Cw721HandleMsg { /// Allows operator to transfer / send the token from the owner's account. /// If expiration is set, then this allowance has a time/height limit Approve { - operator: HumanAddr, + spender: HumanAddr, token_id: String, expires: Option, }, /// Remove previously granted Approval Revoke { - operator: HumanAddr, + spender: HumanAddr, token_id: String, }, /// Allows operator to transfer / send any token from the owner's account. diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs index 81e1cf04f..2c712da6d 100644 --- a/packages/cw721/src/query.rs +++ b/packages/cw721/src/query.rs @@ -49,14 +49,14 @@ pub struct OwnerOfResponse { /// Owner of the token pub owner: HumanAddr, /// If set this address is approved to transfer/send the token as well - pub approval: Option, + pub approvals: Vec, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct Approval { /// Account that can transfer/send the token - pub approved: HumanAddr, - /// When the Approval expires (maybe never) + pub spender: HumanAddr, + /// When the Approval expires (maybe Expiration::never) pub expires: Expiration, } From 539487acc4259887f9b66a2d739c9b7f027c79dc Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 24 Aug 2020 23:08:27 +0200 Subject: [PATCH 10/10] Add Pagination details --- packages/cw721/README.md | 20 ++++++++++---- packages/cw721/schema/cw721_query_msg.json | 32 +++++++++++++++++++++- packages/cw721/schema/tokens_response.json | 1 + packages/cw721/src/helpers.rs | 15 ++++++++-- packages/cw721/src/query.rs | 24 +++++++++++----- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/packages/cw721/README.md b/packages/cw721/README.md index 8d83b9459..c6f32e6b7 100644 --- a/packages/cw721/README.md +++ b/packages/cw721/README.md @@ -100,11 +100,21 @@ want both info to display one NFT. ### Queries -**TODO** Handle paging? Right now, let it just get big and -expensive to query. This API will most likely evolve. - -`Tokens{owner}` - List all token_ids that belong to a given owner. +Pagination is acheived via `start_after` and `limit`. Limit is a request +set by the client, if unset, the contract will automatically set it to +`DefaultLimit` (suggested 10). If set, it will be used up to a `MaxLimit` +value (suggested 30). Contracts can define other `DefaultLimit` and `MaxLimit` +values without violating the CW721 spec, and clients should not rely on +any particular values. + +If `start_after` is unset, the query returns the first results, ordered by +lexogaphically by `token_id`. If `start_after` is set, then it returns the +first `limit` tokens *after* the given one. This allows straight-forward +pagination by taking the last result returned (a `token_id`) and using it +as the `start_after` value in a future query. + +`Tokens{owner, start_after, limit}` - List all token_ids that belong to a given owner. Return type is `TokensResponse{tokens: Vec}`. -`AllTokens{}` - Requires pagination. Lists all token_ids controlled by +`AllTokens{start_after, limit}` - Requires pagination. Lists all token_ids controlled by the contract. diff --git a/packages/cw721/schema/cw721_query_msg.json b/packages/cw721/schema/cw721_query_msg.json index fb965c51d..2b3d0e658 100644 --- a/packages/cw721/schema/cw721_query_msg.json +++ b/packages/cw721/schema/cw721_query_msg.json @@ -119,8 +119,22 @@ "owner" ], "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "owner": { "$ref": "#/definitions/HumanAddr" + }, + "start_after": { + "type": [ + "string", + "null" + ] } } } @@ -134,7 +148,23 @@ ], "properties": { "all_tokens": { - "type": "object" + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } } } } diff --git a/packages/cw721/schema/tokens_response.json b/packages/cw721/schema/tokens_response.json index 1ef6ca312..4c42bea1c 100644 --- a/packages/cw721/schema/tokens_response.json +++ b/packages/cw721/schema/tokens_response.json @@ -7,6 +7,7 @@ ], "properties": { "tokens": { + "description": "Contains all token_ids in lexographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", "type": "array", "items": { "type": "string" diff --git a/packages/cw721/src/helpers.rs b/packages/cw721/src/helpers.rs index c0e934e4b..39787560d 100644 --- a/packages/cw721/src/helpers.rs +++ b/packages/cw721/src/helpers.rs @@ -115,16 +115,25 @@ impl Cw721Contract { &self, querier: &Q, owner: T, + start_after: Option, + limit: Option, ) -> StdResult { let req = Cw721QueryMsg::Tokens { owner: owner.into(), + start_after, + limit, }; self.query(querier, req) } /// With enumerable extension - pub fn all_tokens(&self, querier: &Q) -> StdResult { - let req = Cw721QueryMsg::AllTokens {}; + pub fn all_tokens( + &self, + querier: &Q, + start_after: Option, + limit: Option, + ) -> StdResult { + let req = Cw721QueryMsg::AllTokens { start_after, limit }; self.query(querier, req) } @@ -135,7 +144,7 @@ impl Cw721Contract { /// returns true if the contract supports the enumerable extension pub fn has_enumerable(&self, querier: &Q) -> bool { - self.tokens(querier, self.addr()).is_ok() + self.tokens(querier, self.addr(), None, Some(1)).is_ok() } } diff --git a/packages/cw721/src/query.rs b/packages/cw721/src/query.rs index 2c712da6d..cb7c8bb9c 100644 --- a/packages/cw721/src/query.rs +++ b/packages/cw721/src/query.rs @@ -32,16 +32,18 @@ pub enum Cw721QueryMsg { /// With Enumerable extension. /// Returns all tokens owned by the given address, [] if unset. /// Return type: TokensResponse. - Tokens { owner: HumanAddr }, + Tokens { + owner: HumanAddr, + start_after: Option, + limit: Option, + }, /// With Enumerable extension. /// Requires pagination. Lists all token_ids controlled by the contract. /// Return type: TokensResponse. - AllTokens {}, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct TokensResponse { - pub tokens: Vec, + AllTokens { + start_after: Option, + limit: Option, + }, } #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -97,3 +99,11 @@ pub struct AllNftInfoResponse { /// Data on the token itself, pub info: NftInfoResponse, } + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct TokensResponse { + /// Contains all token_ids in lexographical ordering + /// If there are more than `limit`, use `start_from` in future queries + /// to achieve pagination. + pub tokens: Vec, +}