From deba52fba7175102e617e23d2a8a3da9a6e30c9f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 17:50:17 +0200 Subject: [PATCH 01/12] Migrate state.rs to storage-plus --- contracts/cw721-base/Cargo.toml | 2 +- contracts/cw721-base/src/state.rs | 68 ++++--------------------------- 2 files changed, 9 insertions(+), 61 deletions(-) diff --git a/contracts/cw721-base/Cargo.toml b/contracts/cw721-base/Cargo.toml index f8cda8012..c89ab2255 100644 --- a/contracts/cw721-base/Cargo.toml +++ b/contracts/cw721-base/Cargo.toml @@ -28,8 +28,8 @@ library = [] cw0 = { path = "../../packages/cw0", version = "0.3.0" } cw2 = { path = "../../packages/cw2", version = "0.3.0" } cw721 = { path = "../../packages/cw721", version = "0.3.0" } +cw-storage-plus = { path = "../../packages/storage-plus", version = "0.3.0" , feature = ["iterator"]} cosmwasm-std = { version = "0.11.0" } -cosmwasm-storage = { version = "0.11.0", features = ["iterator"] } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.20" } diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 67947d1da..74fb9354b 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -2,19 +2,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, ReadonlyStorage, StdResult, Storage}; -use cosmwasm_storage::{ - bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, - Singleton, -}; use cw721::{ContractInfoResponse, Expiration}; - -pub const CONFIG_KEY: &[u8] = b"config"; -pub const MINTER_KEY: &[u8] = b"minter"; -pub const CONTRACT_INFO_KEY: &[u8] = b"nft_info"; -pub const NUM_TOKENS_KEY: &[u8] = b"num_tokens"; - -pub const TOKEN_PREFIX: &[u8] = b"tokens"; -pub const OPERATOR_PREFIX: &[u8] = b"operators"; +use cw_storage_plus::{Item, Map}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct TokenInfo { @@ -39,60 +28,19 @@ pub struct Approval { pub expires: Expiration, } -pub fn contract_info(storage: &mut S) -> Singleton { - singleton(storage, CONTRACT_INFO_KEY) -} - -pub fn contract_info_read( - storage: &S, -) -> ReadonlySingleton { - singleton_read(storage, CONTRACT_INFO_KEY) -} - -pub fn mint(storage: &mut S) -> Singleton { - singleton(storage, MINTER_KEY) -} - -pub fn mint_read(storage: &S) -> ReadonlySingleton { - singleton_read(storage, MINTER_KEY) -} - -fn token_count(storage: &mut S) -> Singleton { - singleton(storage, NUM_TOKENS_KEY) -} +pub const CONTRACT_INFO: Item = Item::new(b"nft_info"); +pub const MINTER: Item = Item::new(b"minter"); +pub const TOKEN_COUNT: Item = Item::new(b"num_tokens"); -fn token_count_read(storage: &S) -> ReadonlySingleton { - singleton_read(storage, NUM_TOKENS_KEY) -} +pub const TOKENS: Map<&[u8], TokenInfo> = Map::new(b"tokens"); +pub const OPERATORS: Map<(&[u8], &[u8]), Expiration> = Map::new(b"operators"); pub fn num_tokens(storage: &S) -> StdResult { - Ok(token_count_read(storage).may_load()?.unwrap_or_default()) + Ok(TOKEN_COUNT.may_load(storage)?.unwrap_or_default()) } pub fn increment_tokens(storage: &mut S) -> StdResult { let val = num_tokens(storage)? + 1; - token_count(storage).save(&val)?; + TOKEN_COUNT.save(storage, &val)?; Ok(val) } - -pub fn tokens(storage: &mut S) -> Bucket { - bucket(storage, TOKEN_PREFIX) -} - -pub fn tokens_read(storage: &S) -> ReadonlyBucket { - bucket_read(storage, TOKEN_PREFIX) -} - -pub fn operators<'a, S: Storage>( - storage: &'a mut S, - owner: &CanonicalAddr, -) -> Bucket<'a, S, Expiration> { - Bucket::multilevel(storage, &[OPERATOR_PREFIX, owner.as_slice()]) -} - -pub fn operators_read<'a, S: ReadonlyStorage>( - storage: &'a S, - owner: &CanonicalAddr, -) -> ReadonlyBucket<'a, S, Expiration> { - ReadonlyBucket::multilevel(storage, &[OPERATOR_PREFIX, owner.as_slice()]) -} From 9a994416488da33f2dcb4a26d5ee97a757c9bdbc Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:03:16 +0200 Subject: [PATCH 02/12] cw721 compiles with storage-plus --- Cargo.lock | 2 +- contracts/cw721-base/Cargo.toml | 2 +- contracts/cw721-base/src/contract.rs | 66 +++++++++++++++------------- contracts/cw721-base/src/state.rs | 4 +- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d0c7480d2..68cc86c8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,7 +299,7 @@ version = "0.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", + "cw-storage-plus", "cw0", "cw2", "cw721", diff --git a/contracts/cw721-base/Cargo.toml b/contracts/cw721-base/Cargo.toml index c89ab2255..e9390c035 100644 --- a/contracts/cw721-base/Cargo.toml +++ b/contracts/cw721-base/Cargo.toml @@ -28,7 +28,7 @@ library = [] cw0 = { path = "../../packages/cw0", version = "0.3.0" } cw2 = { path = "../../packages/cw2", version = "0.3.0" } cw721 = { path = "../../packages/cw721", version = "0.3.0" } -cw-storage-plus = { path = "../../packages/storage-plus", version = "0.3.0" , feature = ["iterator"]} +cw-storage-plus = { path = "../../packages/storage-plus", version = "0.3.0" , features = ["iterator"]} cosmwasm-std = { version = "0.11.0" } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index 91c010183..25f9062a1 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{ HumanAddr, InitResponse, MessageInfo, Order, Querier, StdResult, Storage, KV, }; -use cw0::{calc_range_start_human, calc_range_start_string}; +use cw0::maybe_canonical; use cw2::set_contract_version; use cw721::{ AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Expiration, NftInfoResponse, @@ -13,9 +13,9 @@ use cw721::{ use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, MintMsg, MinterResponse, QueryMsg}; use crate::state::{ - contract_info, contract_info_read, increment_tokens, mint, mint_read, num_tokens, operators, - operators_read, tokens, tokens_read, Approval, TokenInfo, + increment_tokens, num_tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS, TOKENS, }; +use cw_storage_plus::{Bound, OwnedBound}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw721-base"; @@ -33,9 +33,9 @@ pub fn init( name: msg.name, symbol: msg.symbol, }; - contract_info(&mut deps.storage).save(&info)?; + CONTRACT_INFO.save(&mut deps.storage, &info)?; let minter = deps.api.canonical_address(&msg.minter)?; - mint(&mut deps.storage).save(&minter)?; + MINTER.save(&mut deps.storage, &minter)?; Ok(InitResponse::default()) } @@ -77,7 +77,7 @@ pub fn handle_mint( info: MessageInfo, msg: MintMsg, ) -> Result { - let minter = mint(&mut deps.storage).load()?; + let minter = MINTER.load(&deps.storage)?; let sender_raw = deps.api.canonical_address(&info.sender)?; if sender_raw != minter { @@ -92,10 +92,14 @@ pub fn handle_mint( description: msg.description.unwrap_or_default(), image: msg.image, }; - tokens(&mut deps.storage).update(msg.token_id.as_bytes(), |old| match old { - Some(_) => Err(ContractError::Claimed {}), - None => Ok(token), - })?; + TOKENS.update( + &mut deps.storage, + msg.token_id.as_bytes(), + |old| match old { + Some(_) => Err(ContractError::Claimed {}), + None => Ok(token), + }, + )?; increment_tokens(&mut deps.storage)?; @@ -168,13 +172,13 @@ pub fn _transfer_nft( recipient: &HumanAddr, token_id: &str, ) -> Result { - let mut token = tokens(&mut deps.storage).load(token_id.as_bytes())?; + let mut token = TOKENS.load(&mut deps.storage, token_id.as_bytes())?; // ensure we have permissions check_can_send(&deps, env, info, &token)?; // set owner and remove existing approvals token.owner = deps.api.canonical_address(recipient)?; token.approvals = vec![]; - tokens(&mut deps.storage).save(token_id.as_bytes(), &token)?; + TOKENS.save(&mut deps.storage, token_id.as_bytes(), &token)?; Ok(token) } @@ -231,7 +235,7 @@ pub fn _update_approvals( add: bool, expires: Option, ) -> Result { - let mut token = tokens(&mut deps.storage).load(token_id.as_bytes())?; + let mut token = TOKENS.load(&mut deps.storage, token_id.as_bytes())?; // ensure we have permissions check_can_approve(&deps, env, info, &token)?; @@ -257,7 +261,7 @@ pub fn _update_approvals( token.approvals.push(approval); } - tokens(&mut deps.storage).save(token_id.as_bytes(), &token)?; + TOKENS.save(&mut deps.storage, token_id.as_bytes(), &token)?; Ok(token) } @@ -278,7 +282,7 @@ pub fn handle_approve_all( // set the operator for us let sender_raw = deps.api.canonical_address(&info.sender)?; let operator_raw = deps.api.canonical_address(&operator)?; - operators(&mut deps.storage, &sender_raw).save(operator_raw.as_slice(), &expires)?; + OPERATORS.save(&mut deps.storage, (&sender_raw, &operator_raw), &expires)?; Ok(HandleResponse { messages: vec![], @@ -299,7 +303,7 @@ pub fn handle_revoke_all( ) -> Result { let sender_raw = deps.api.canonical_address(&info.sender)?; let operator_raw = deps.api.canonical_address(&operator)?; - operators(&mut deps.storage, &sender_raw).remove(operator_raw.as_slice()); + OPERATORS.remove(&mut deps.storage, (&sender_raw, &operator_raw)); Ok(HandleResponse { messages: vec![], @@ -325,7 +329,7 @@ fn check_can_approve( return Ok(()); } // operator can approve - let op = operators_read(&deps.storage, &token.owner).may_load(sender_raw.as_slice())?; + let op = OPERATORS.may_load(&deps.storage, (&token.owner, &sender_raw))?; match op { Some(ex) => { if ex.is_expired(&env.block) { @@ -361,7 +365,7 @@ fn check_can_send( } // operator can send - let op = operators_read(&deps.storage, &token.owner).may_load(sender_raw.as_slice())?; + let op = OPERATORS.may_load(&deps.storage, (&token.owner, &sender_raw))?; match op { Some(ex) => { if ex.is_expired(&env.block) { @@ -424,7 +428,7 @@ pub fn query( fn query_minter( deps: &Extern, ) -> StdResult { - let minter_raw = mint_read(&deps.storage).load()?; + let minter_raw = MINTER.load(&deps.storage)?; let minter = deps.api.human_address(&minter_raw)?; Ok(MinterResponse { minter }) } @@ -432,7 +436,7 @@ fn query_minter( fn query_contract_info( deps: &Extern, ) -> StdResult { - contract_info_read(&deps.storage).load() + CONTRACT_INFO.load(&deps.storage) } fn query_num_tokens( @@ -446,7 +450,7 @@ fn query_nft_info( deps: &Extern, token_id: String, ) -> StdResult { - let info = tokens_read(&deps.storage).load(token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; Ok(NftInfoResponse { name: info.name, description: info.description, @@ -460,7 +464,7 @@ fn query_owner_of( token_id: String, include_expired: bool, ) -> StdResult { - let info = tokens_read(&deps.storage).load(token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; Ok(OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, approvals: humanize_approvals(deps.api, &env.block, &info, include_expired)?, @@ -478,12 +482,14 @@ fn query_all_approvals( start_after: Option, limit: Option, ) -> StdResult { - let owner_raw = deps.api.canonical_address(&owner)?; let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = calc_range_start_human(deps.api, start_after)?; + let start_canon = maybe_canonical(deps.api, start_after)?; + let start = OwnedBound::exclusive(start_canon); - let res: StdResult> = operators_read(&deps.storage, &owner_raw) - .range(start.as_deref(), None, Order::Ascending) + let owner_raw = deps.api.canonical_address(&owner)?; + let res: StdResult> = OPERATORS + .prefix(&owner_raw) + .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) .filter(|r| include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block)) .take(limit) .map(|item| parse_approval(deps.api, item)) @@ -504,10 +510,10 @@ fn query_all_tokens( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = calc_range_start_string(start_after); + let start = OwnedBound::exclusive(start_after.map(Vec::from)); - let tokens: StdResult> = tokens_read(&deps.storage) - .range(start.as_deref(), None, Order::Ascending) + let tokens: StdResult> = TOKENS + .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) .take(limit) .map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string())) .collect(); @@ -520,7 +526,7 @@ fn query_all_nft_info( token_id: String, include_expired: bool, ) -> StdResult { - let info = tokens_read(&deps.storage).load(token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; Ok(AllNftInfoResponse { access: OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 74fb9354b..6833c1d9c 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{CanonicalAddr, ReadonlyStorage, StdResult, Storage}; +use cosmwasm_std::{CanonicalAddr, StdResult, Storage}; use cw721::{ContractInfoResponse, Expiration}; use cw_storage_plus::{Item, Map}; @@ -35,7 +35,7 @@ pub const TOKEN_COUNT: Item = Item::new(b"num_tokens"); pub const TOKENS: Map<&[u8], TokenInfo> = Map::new(b"tokens"); pub const OPERATORS: Map<(&[u8], &[u8]), Expiration> = Map::new(b"operators"); -pub fn num_tokens(storage: &S) -> StdResult { +pub fn num_tokens(storage: &S) -> StdResult { Ok(TOKEN_COUNT.may_load(storage)?.unwrap_or_default()) } From 4b5b7b1b11915cc942acbc3a3e80b1e1c7b362a0 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:14:43 +0200 Subject: [PATCH 03/12] Add support for string keys in map --- packages/storage-plus/src/keys.rs | 48 ++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/storage-plus/src/keys.rs b/packages/storage-plus/src/keys.rs index 1a07ff0ee..759a54c97 100644 --- a/packages/storage-plus/src/keys.rs +++ b/packages/storage-plus/src/keys.rs @@ -1,6 +1,8 @@ +use std::marker::PhantomData; +use std::str::from_utf8; + use crate::helpers::{decode_length, namespaces_with_key}; use crate::Endian; -use std::marker::PhantomData; // pub trait PrimaryKey<'a>: Copy { pub trait PrimaryKey<'a>: Clone { @@ -25,6 +27,8 @@ type Pk0 = (); type Pk1<'a> = &'a [u8]; type Pk2<'a, T = &'a [u8], U = &'a [u8]> = (T, U); +type PkStr<'a> = &'a str; + impl<'a> PrimaryKey<'a> for Pk1<'a> { type Prefix = Pk0; @@ -38,6 +42,20 @@ impl<'a> PrimaryKey<'a> for Pk1<'a> { } } +// Provide a string version of this to raw encode strings +impl<'a> PrimaryKey<'a> for PkStr<'a> { + type Prefix = Pk0; + + fn key<'b>(&'b self) -> Vec<&'b [u8]> { + // this is simple, we don't add more prefixes + vec![self.as_bytes()] + } + + fn parse_key(serialized: &'a [u8]) -> Self { + from_utf8(serialized).unwrap() + } +} + // use generics for combining there - so we can use &[u8], PkOwned, or IntKey impl<'a, T: PrimaryKey<'a> + Prefixer<'a>, U: PrimaryKey<'a>> PrimaryKey<'a> for (T, U) { type Prefix = T; @@ -80,6 +98,13 @@ impl<'a> Prefixer<'a> for Pk2<'a> { } } +// Provide a string version of this to raw encode strings +impl<'a> Prefixer<'a> for PkStr<'a> { + fn prefix<'b>(&'b self) -> Vec<&'b [u8]> { + vec![self.as_bytes()] + } +} + // this is a marker for the Map.range() helper, so we can detect () in Generic bounds pub trait EmptyPrefix { fn new() -> Self; @@ -198,6 +223,27 @@ mod test { assert_eq!(4242u32.to_be_bytes().to_vec(), path[0].to_vec()); } + #[test] + fn str_key_works() { + let k: &str = "hello"; + let path = k.key(); + assert_eq!(1, path.len()); + assert_eq!("hello".as_bytes(), path[0]); + + let joined = k.joined_key(); + let parsed = PkStr::parse_key(&joined); + assert_eq!(parsed, "hello"); + } + + #[test] + fn nested_str_key_works() { + let k: (&str, &[u8]) = ("hello", b"world"); + let path = k.key(); + assert_eq!(2, path.len()); + assert_eq!("hello".as_bytes(), path[0]); + assert_eq!("world".as_bytes(), path[1]); + } + #[test] fn composite_byte_key() { let k: (&[u8], &[u8]) = (b"foo", b"bar"); From a933cb9fb550e8bbaccf35fae9c07b63f357bc73 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:19:55 +0200 Subject: [PATCH 04/12] Simplify nft state using &str as key --- contracts/cw721-base/src/contract.rs | 26 +++++++++++--------------- contracts/cw721-base/src/state.rs | 2 +- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index 25f9062a1..b2c688145 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -92,14 +92,10 @@ pub fn handle_mint( description: msg.description.unwrap_or_default(), image: msg.image, }; - TOKENS.update( - &mut deps.storage, - msg.token_id.as_bytes(), - |old| match old { - Some(_) => Err(ContractError::Claimed {}), - None => Ok(token), - }, - )?; + TOKENS.update(&mut deps.storage, &msg.token_id, |old| match old { + Some(_) => Err(ContractError::Claimed {}), + None => Ok(token), + })?; increment_tokens(&mut deps.storage)?; @@ -172,13 +168,13 @@ pub fn _transfer_nft( recipient: &HumanAddr, token_id: &str, ) -> Result { - let mut token = TOKENS.load(&mut deps.storage, token_id.as_bytes())?; + let mut token = TOKENS.load(&deps.storage, &token_id)?; // ensure we have permissions check_can_send(&deps, env, info, &token)?; // set owner and remove existing approvals token.owner = deps.api.canonical_address(recipient)?; token.approvals = vec![]; - TOKENS.save(&mut deps.storage, token_id.as_bytes(), &token)?; + TOKENS.save(&mut deps.storage, &token_id, &token)?; Ok(token) } @@ -235,7 +231,7 @@ pub fn _update_approvals( add: bool, expires: Option, ) -> Result { - let mut token = TOKENS.load(&mut deps.storage, token_id.as_bytes())?; + let mut token = TOKENS.load(&deps.storage, &token_id)?; // ensure we have permissions check_can_approve(&deps, env, info, &token)?; @@ -261,7 +257,7 @@ pub fn _update_approvals( token.approvals.push(approval); } - TOKENS.save(&mut deps.storage, token_id.as_bytes(), &token)?; + TOKENS.save(&mut deps.storage, &token_id, &token)?; Ok(token) } @@ -450,7 +446,7 @@ fn query_nft_info( deps: &Extern, token_id: String, ) -> StdResult { - let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, &token_id)?; Ok(NftInfoResponse { name: info.name, description: info.description, @@ -464,7 +460,7 @@ fn query_owner_of( token_id: String, include_expired: bool, ) -> StdResult { - let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, &token_id)?; Ok(OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, approvals: humanize_approvals(deps.api, &env.block, &info, include_expired)?, @@ -526,7 +522,7 @@ fn query_all_nft_info( token_id: String, include_expired: bool, ) -> StdResult { - let info = TOKENS.load(&deps.storage, token_id.as_bytes())?; + let info = TOKENS.load(&deps.storage, &token_id)?; Ok(AllNftInfoResponse { access: OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 6833c1d9c..3c09929d3 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -32,7 +32,7 @@ pub const CONTRACT_INFO: Item = Item::new(b"nft_info"); pub const MINTER: Item = Item::new(b"minter"); pub const TOKEN_COUNT: Item = Item::new(b"num_tokens"); -pub const TOKENS: Map<&[u8], TokenInfo> = Map::new(b"tokens"); +pub const TOKENS: Map<&str, TokenInfo> = Map::new(b"tokens"); pub const OPERATORS: Map<(&[u8], &[u8]), Expiration> = Map::new(b"operators"); pub fn num_tokens(storage: &S) -> StdResult { From 9dd770c6f418d5f8c8fa671977d0cc0e779470d2 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:36:13 +0200 Subject: [PATCH 05/12] Use IndexedMap for tokens --- contracts/cw721-base/src/contract.rs | 22 ++++++++++++---------- contracts/cw721-base/src/state.rs | 11 +++++++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index b2c688145..d09dfb39a 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -13,7 +13,7 @@ use cw721::{ use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, MintMsg, MinterResponse, QueryMsg}; use crate::state::{ - increment_tokens, num_tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS, TOKENS, + increment_tokens, num_tokens, tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS, }; use cw_storage_plus::{Bound, OwnedBound}; @@ -92,7 +92,7 @@ pub fn handle_mint( description: msg.description.unwrap_or_default(), image: msg.image, }; - TOKENS.update(&mut deps.storage, &msg.token_id, |old| match old { + tokens().update(&mut deps.storage, &msg.token_id, |old| match old { Some(_) => Err(ContractError::Claimed {}), None => Ok(token), })?; @@ -168,13 +168,13 @@ pub fn _transfer_nft( recipient: &HumanAddr, token_id: &str, ) -> Result { - let mut token = TOKENS.load(&deps.storage, &token_id)?; + let mut token = tokens().load(&deps.storage, &token_id)?; // ensure we have permissions check_can_send(&deps, env, info, &token)?; // set owner and remove existing approvals token.owner = deps.api.canonical_address(recipient)?; token.approvals = vec![]; - TOKENS.save(&mut deps.storage, &token_id, &token)?; + tokens().save(&mut deps.storage, &token_id, &token)?; Ok(token) } @@ -231,7 +231,7 @@ pub fn _update_approvals( add: bool, expires: Option, ) -> Result { - let mut token = TOKENS.load(&deps.storage, &token_id)?; + let mut token = tokens().load(&deps.storage, &token_id)?; // ensure we have permissions check_can_approve(&deps, env, info, &token)?; @@ -257,7 +257,7 @@ pub fn _update_approvals( token.approvals.push(approval); } - TOKENS.save(&mut deps.storage, &token_id, &token)?; + tokens().save(&mut deps.storage, &token_id, &token)?; Ok(token) } @@ -446,7 +446,7 @@ fn query_nft_info( deps: &Extern, token_id: String, ) -> StdResult { - let info = TOKENS.load(&deps.storage, &token_id)?; + let info = tokens().load(&deps.storage, &token_id)?; Ok(NftInfoResponse { name: info.name, description: info.description, @@ -460,7 +460,7 @@ fn query_owner_of( token_id: String, include_expired: bool, ) -> StdResult { - let info = TOKENS.load(&deps.storage, &token_id)?; + let info = tokens().load(&deps.storage, &token_id)?; Ok(OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, approvals: humanize_approvals(deps.api, &env.block, &info, include_expired)?, @@ -508,7 +508,9 @@ fn query_all_tokens( let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = OwnedBound::exclusive(start_after.map(Vec::from)); - let tokens: StdResult> = TOKENS + // TODO: make range top-level + let tokens: StdResult> = tokens::() + .prefix(()) .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) .take(limit) .map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string())) @@ -522,7 +524,7 @@ fn query_all_nft_info( token_id: String, include_expired: bool, ) -> StdResult { - let info = TOKENS.load(&deps.storage, &token_id)?; + let info = tokens().load(&deps.storage, &token_id)?; Ok(AllNftInfoResponse { access: OwnerOfResponse { owner: deps.api.human_address(&info.owner)?, diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 3c09929d3..30fba88bc 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, StdResult, Storage}; use cw721::{ContractInfoResponse, Expiration}; -use cw_storage_plus::{Item, Map}; +use cw_storage_plus::{IndexedMap, Item, Map}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct TokenInfo { @@ -32,7 +32,7 @@ pub const CONTRACT_INFO: Item = Item::new(b"nft_info"); pub const MINTER: Item = Item::new(b"minter"); pub const TOKEN_COUNT: Item = Item::new(b"num_tokens"); -pub const TOKENS: Map<&str, TokenInfo> = Map::new(b"tokens"); +// pub const TOKENS: Map<&str, TokenInfo> = Map::new(b"tokens"); pub const OPERATORS: Map<(&[u8], &[u8]), Expiration> = Map::new(b"operators"); pub fn num_tokens(storage: &S) -> StdResult { @@ -44,3 +44,10 @@ pub fn increment_tokens(storage: &mut S) -> StdResult { TOKEN_COUNT.save(storage, &val)?; Ok(val) } + +// indexed map needs function, not const (for now at least) +pub fn tokens<'a, S: Storage + 'a>() -> IndexedMap<'a, 'a, &'a str, TokenInfo, S> { + IndexedMap::<&str, TokenInfo, S>::new(b"tokens") + .with_index("owner", b"tokens__owner", |d| d.owner.to_vec()) + .unwrap() +} From ead5b8bed87d43b7b7937870a0a8342c3b497f1b Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:38:20 +0200 Subject: [PATCH 06/12] Expose range top-level on IndexedMap --- contracts/cw721-base/src/contract.rs | 2 -- packages/storage-plus/src/indexed_map.rs | 28 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index d09dfb39a..dc0d296fa 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -508,9 +508,7 @@ fn query_all_tokens( let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = OwnedBound::exclusive(start_after.map(Vec::from)); - // TODO: make range top-level let tokens: StdResult> = tokens::() - .prefix(()) .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) .take(limit) .map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string())) diff --git a/packages/storage-plus/src/indexed_map.rs b/packages/storage-plus/src/indexed_map.rs index 9aa7af0cc..011fa7293 100644 --- a/packages/storage-plus/src/indexed_map.rs +++ b/packages/storage-plus/src/indexed_map.rs @@ -7,9 +7,9 @@ use serde::Serialize; use std::marker::PhantomData; use crate::indexes::Index; -use crate::keys::{Prefixer, PrimaryKey}; +use crate::keys::{EmptyPrefix, Prefixer, PrimaryKey}; use crate::map::Map; -use crate::prefix::Prefix; +use crate::prefix::{Bound, Prefix}; pub trait IndexList { fn get_indexes(&'_ self) -> Box> + '_>; @@ -134,6 +134,30 @@ where } } +// short-cut for simple keys, rather than .prefix(()).range(...) +impl<'a, 'x, K, T, S> IndexedMap<'a, 'x, K, T, S> +where + K: PrimaryKey<'a>, + T: Serialize + DeserializeOwned + Clone + 'x, + S: Storage + 'x, + K::Prefix: EmptyPrefix, +{ + // I would prefer not to copy code from Prefix, but no other way + // with lifetimes (create Prefix inside function and return ref = no no) + pub fn range<'c>( + &self, + store: &'c S, + min: Bound<'_>, + max: Bound<'_>, + order: cosmwasm_std::Order, + ) -> Box>> + 'c> + where + T: 'c, + { + self.prefix(K::Prefix::new()).range(store, min, max, order) + } +} + #[cfg(test)] mod test { use super::*; From da6038c82cb99360ecf68d6c6241ebe5e3999434 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 13 Oct 2020 18:50:53 +0200 Subject: [PATCH 07/12] Add QueryMsg::Tokens{} support - untested --- contracts/cw721-base/schema/query_msg.json | 34 ++++++++++++++++++++++ contracts/cw721-base/src/contract.rs | 32 ++++++++++++++++++-- contracts/cw721-base/src/msg.rs | 8 +++++ contracts/cw721-base/src/state.rs | 4 ++- 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/contracts/cw721-base/schema/query_msg.json b/contracts/cw721-base/schema/query_msg.json index 743214587..06f5c55a3 100644 --- a/contracts/cw721-base/schema/query_msg.json +++ b/contracts/cw721-base/schema/query_msg.json @@ -145,6 +145,40 @@ } } }, + { + "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" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "$ref": "#/definitions/HumanAddr" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + } + } + } + }, { "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract. Return type: TokensResponse.", "type": "object", diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index dc0d296fa..d6c8c85b1 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ attr, from_binary, to_binary, Api, Binary, BlockInfo, CosmosMsg, Env, Extern, HandleResponse, - HumanAddr, InitResponse, MessageInfo, Order, Querier, StdResult, Storage, KV, + HumanAddr, InitResponse, MessageInfo, Order, Querier, StdError, StdResult, Storage, KV, }; use cw0::maybe_canonical; @@ -13,7 +13,8 @@ use cw721::{ use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, MintMsg, MinterResponse, QueryMsg}; use crate::state::{ - increment_tokens, num_tokens, tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS, + increment_tokens, num_tokens, tokens, Approval, TokenInfo, CONTRACT_INFO, IDX_OWNER, MINTER, + OPERATORS, }; use cw_storage_plus::{Bound, OwnedBound}; @@ -415,6 +416,11 @@ pub fn query( limit, )?), QueryMsg::NumTokens {} => to_binary(&query_num_tokens(deps)?), + QueryMsg::Tokens { + owner, + start_after, + limit, + } => to_binary(&query_tokens(deps, owner, start_after, limit)?), QueryMsg::AllTokens { start_after, limit } => { to_binary(&query_all_tokens(deps, start_after, limit)?) } @@ -500,6 +506,28 @@ fn parse_approval(api: A, item: StdResult>) -> StdResult< }) } +fn query_tokens( + deps: &Extern, + owner: HumanAddr, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + // TODO: get pagination working on second indexes + let _start = OwnedBound::exclusive(start_after.map(Vec::from)); + + let owner_raw = deps.api.canonical_address(&owner)?; + let tokens: Result, _> = tokens::() + .pks_by_index(&deps.storage, IDX_OWNER, &owner_raw)? + // .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) + .take(limit) + .map(String::from_utf8) + .collect(); + // TODO: make issue on CosmWasm to add impl From for StdError + let tokens = tokens.map_err(StdError::invalid_utf8)?; + Ok(TokensResponse { tokens }) +} + fn query_all_tokens( deps: &Extern, start_after: Option, diff --git a/contracts/cw721-base/src/msg.rs b/contracts/cw721-base/src/msg.rs index 023771c5c..92359c517 100644 --- a/contracts/cw721-base/src/msg.rs +++ b/contracts/cw721-base/src/msg.rs @@ -113,6 +113,14 @@ pub enum QueryMsg { include_expired: Option, }, + /// With Enumerable extension. + /// Returns all tokens owned by the given address, [] if unset. + /// Return type: TokensResponse. + Tokens { + owner: HumanAddr, + start_after: Option, + limit: Option, + }, /// With Enumerable extension. /// Requires pagination. Lists all token_ids controlled by the contract. /// Return type: TokensResponse. diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 30fba88bc..85e407233 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -45,9 +45,11 @@ pub fn increment_tokens(storage: &mut S) -> StdResult { Ok(val) } +pub const IDX_OWNER: &str = "owner"; + // indexed map needs function, not const (for now at least) pub fn tokens<'a, S: Storage + 'a>() -> IndexedMap<'a, 'a, &'a str, TokenInfo, S> { IndexedMap::<&str, TokenInfo, S>::new(b"tokens") - .with_index("owner", b"tokens__owner", |d| d.owner.to_vec()) + .with_index(IDX_OWNER, b"tokens__owner", |d| d.owner.to_vec()) .unwrap() } From 0d216921f574b482b35a1cc3bb0ef176ef944ff1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Oct 2020 16:49:56 +0200 Subject: [PATCH 08/12] Updates after rebase --- packages/storage-plus/src/indexed_map.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/storage-plus/src/indexed_map.rs b/packages/storage-plus/src/indexed_map.rs index 011fa7293..a24ac62c1 100644 --- a/packages/storage-plus/src/indexed_map.rs +++ b/packages/storage-plus/src/indexed_map.rs @@ -135,11 +135,12 @@ where } // short-cut for simple keys, rather than .prefix(()).range(...) -impl<'a, 'x, K, T, S> IndexedMap<'a, 'x, K, T, S> +impl<'a, K, T, S, I> IndexedMap<'a, K, T, S, I> where K: PrimaryKey<'a>, - T: Serialize + DeserializeOwned + Clone + 'x, - S: Storage + 'x, + T: Serialize + DeserializeOwned + Clone, + S: Storage, + I: IndexList, K::Prefix: EmptyPrefix, { // I would prefer not to copy code from Prefix, but no other way @@ -147,8 +148,8 @@ where pub fn range<'c>( &self, store: &'c S, - min: Bound<'_>, - max: Bound<'_>, + min: Option, + max: Option, order: cosmwasm_std::Order, ) -> Box>> + 'c> where From e07c66e4de3d947e81ddd1363492b472c0a22924 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Oct 2020 16:59:09 +0200 Subject: [PATCH 09/12] Update cw721-base with new index format --- contracts/cw721-base/src/contract.rs | 21 +++++++++++---------- contracts/cw721-base/src/state.rs | 23 ++++++++++++++++------- packages/storage-plus/src/lib.rs | 2 +- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index d6c8c85b1..754b534c0 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -13,10 +13,9 @@ use cw721::{ use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, MintMsg, MinterResponse, QueryMsg}; use crate::state::{ - increment_tokens, num_tokens, tokens, Approval, TokenInfo, CONTRACT_INFO, IDX_OWNER, MINTER, - OPERATORS, + increment_tokens, num_tokens, tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS, }; -use cw_storage_plus::{Bound, OwnedBound}; +use cw_storage_plus::Bound; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw721-base"; @@ -486,12 +485,12 @@ fn query_all_approvals( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start_canon = maybe_canonical(deps.api, start_after)?; - let start = OwnedBound::exclusive(start_canon); + let start = start_canon.map(Bound::exclusive); let owner_raw = deps.api.canonical_address(&owner)?; let res: StdResult> = OPERATORS .prefix(&owner_raw) - .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) + .range(&deps.storage, start, None, Order::Ascending) .filter(|r| include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block)) .take(limit) .map(|item| parse_approval(deps.api, item)) @@ -514,12 +513,14 @@ fn query_tokens( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; // TODO: get pagination working on second indexes - let _start = OwnedBound::exclusive(start_after.map(Vec::from)); + let _start = start_after.map(Bound::exclusive); let owner_raw = deps.api.canonical_address(&owner)?; let tokens: Result, _> = tokens::() - .pks_by_index(&deps.storage, IDX_OWNER, &owner_raw)? - // .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) + .idx + .owner + .pks(&deps.storage, &owner_raw) + // .range(&deps.storage, start.bound(), None, Order::Ascending) .take(limit) .map(String::from_utf8) .collect(); @@ -534,10 +535,10 @@ fn query_all_tokens( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = OwnedBound::exclusive(start_after.map(Vec::from)); + let start = start_after.map(Bound::exclusive); let tokens: StdResult> = tokens::() - .range(&deps.storage, start.bound(), Bound::None, Order::Ascending) + .range(&deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string())) .collect(); diff --git a/contracts/cw721-base/src/state.rs b/contracts/cw721-base/src/state.rs index 85e407233..1180f8de9 100644 --- a/contracts/cw721-base/src/state.rs +++ b/contracts/cw721-base/src/state.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, StdResult, Storage}; use cw721::{ContractInfoResponse, Expiration}; -use cw_storage_plus::{IndexedMap, Item, Map}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct TokenInfo { @@ -45,11 +45,20 @@ pub fn increment_tokens(storage: &mut S) -> StdResult { Ok(val) } -pub const IDX_OWNER: &str = "owner"; +pub struct TokenIndexes<'a, S: Storage> { + pub owner: MultiIndex<'a, S, TokenInfo>, +} + +impl<'a, S: Storage> IndexList for TokenIndexes<'a, S> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner]; + Box::new(v.into_iter()) + } +} -// indexed map needs function, not const (for now at least) -pub fn tokens<'a, S: Storage + 'a>() -> IndexedMap<'a, 'a, &'a str, TokenInfo, S> { - IndexedMap::<&str, TokenInfo, S>::new(b"tokens") - .with_index(IDX_OWNER, b"tokens__owner", |d| d.owner.to_vec()) - .unwrap() +pub fn tokens<'a, S: Storage>() -> IndexedMap<'a, &'a str, TokenInfo, S, TokenIndexes<'a, S>> { + let indexes = TokenIndexes { + owner: MultiIndex::new(|d| d.owner.to_vec(), b"tokens", b"tokens__owner"), + }; + IndexedMap::new(b"tokens", indexes) } diff --git a/packages/storage-plus/src/lib.rs b/packages/storage-plus/src/lib.rs index d8bd42744..4e5066175 100644 --- a/packages/storage-plus/src/lib.rs +++ b/packages/storage-plus/src/lib.rs @@ -11,7 +11,7 @@ mod prefix; pub use endian::Endian; #[cfg(feature = "iterator")] -pub use indexed_map::IndexedMap; +pub use indexed_map::{IndexList, IndexedMap}; #[cfg(feature = "iterator")] pub use indexes::{index_int, index_string, Index, MultiIndex, UniqueIndex}; pub use item::Item; From f64678c4f85cdf3a4116472ec9dc00747f6de0e8 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Oct 2020 17:51:41 +0200 Subject: [PATCH 10/12] Use pagination in tokens by owner --- contracts/cw721-base/src/contract.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index 754b534c0..4acfa16e1 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -512,15 +512,13 @@ fn query_tokens( limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - // TODO: get pagination working on second indexes - let _start = start_after.map(Bound::exclusive); + let start = start_after.map(Bound::exclusive); let owner_raw = deps.api.canonical_address(&owner)?; let tokens: Result, _> = tokens::() .idx .owner - .pks(&deps.storage, &owner_raw) - // .range(&deps.storage, start.bound(), None, Order::Ascending) + .pks(&deps.storage, &owner_raw, start, None, Order::Ascending) .take(limit) .map(String::from_utf8) .collect(); From 64656252e523dad0707a94b14120bcd2a89d543c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Oct 2020 18:01:06 +0200 Subject: [PATCH 11/12] Resolve some TODOs --- contracts/cw721-base/src/contract.rs | 1 - packages/storage-plus/src/indexes.rs | 1 - packages/storage-plus/src/keys.rs | 23 ++++++++++++++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index 4acfa16e1..22fe2ecd2 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -522,7 +522,6 @@ fn query_tokens( .take(limit) .map(String::from_utf8) .collect(); - // TODO: make issue on CosmWasm to add impl From for StdError let tokens = tokens.map_err(StdError::invalid_utf8)?; Ok(TokensResponse { tokens }) } diff --git a/packages/storage-plus/src/indexes.rs b/packages/storage-plus/src/indexes.rs index 244b346d7..899ee3e88 100644 --- a/packages/storage-plus/src/indexes.rs +++ b/packages/storage-plus/src/indexes.rs @@ -37,7 +37,6 @@ where S: Storage, T: Serialize + DeserializeOwned + Clone, { - // TODO: pk: PrimaryKey not just &[u8] ??? fn save(&self, store: &mut S, pk: &[u8], data: &T) -> StdResult<()>; fn remove(&self, store: &mut S, pk: &[u8], old_data: &T) -> StdResult<()>; } diff --git a/packages/storage-plus/src/keys.rs b/packages/storage-plus/src/keys.rs index 759a54c97..ecc68a680 100644 --- a/packages/storage-plus/src/keys.rs +++ b/packages/storage-plus/src/keys.rs @@ -115,7 +115,7 @@ impl EmptyPrefix for () { } // Add support for an dynamic keys - constructor functions below -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct PkOwned(pub Vec); impl<'a> PrimaryKey<'a> for PkOwned { @@ -167,7 +167,7 @@ pub type U128Key = IntKey; /// let k = U64Key::new(12345); /// let k = U32Key::from(12345); /// let k: U16Key = 12345.into(); -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct IntKey { pub wrapped: PkOwned, pub data: PhantomData, @@ -299,5 +299,22 @@ mod test { assert_eq!(key, parsed); } - // TODO: parse joined with int/owned keys + #[test] + fn parse_joined_keys_int() { + let key: U64Key = 12345678.into(); + let joined = key.joined_key(); + assert_eq!(8, joined.len()); + let parsed = U64Key::parse_key(&joined); + assert_eq!(key, parsed); + } + + #[test] + fn parse_joined_keys_string_int() { + let key: (U32Key, &str) = (54321.into(), "random"); + let joined = key.joined_key(); + assert_eq!(2 + 4 + 6, joined.len()); + let parsed = <(U32Key, &str)>::parse_key(&joined); + assert_eq!(key, parsed); + assert_eq!("random", parsed.1); + } } From f489ce03444fb655ab18adb76aeafd19eb7fa0ed Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 16 Oct 2020 19:21:12 +0200 Subject: [PATCH 12/12] Test query tokens by owner --- contracts/cw721-base/src/contract.rs | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/contracts/cw721-base/src/contract.rs b/contracts/cw721-base/src/contract.rs index 22fe2ecd2..858b00135 100644 --- a/contracts/cw721-base/src/contract.rs +++ b/contracts/cw721-base/src/contract.rs @@ -1100,4 +1100,73 @@ mod tests { let res = query_all_approvals(&deps, late_env, "person".into(), false, None, None).unwrap(); assert_eq!(0, res.operators.len()); } + + #[test] + fn query_tokens_by_owner() { + let mut deps = mock_dependencies(&[]); + setup_contract(&mut deps); + let minter = mock_info(MINTER, &[]); + + // Mint a couple tokens (from the same owner) + let token_id1 = "grow1".to_string(); + let demeter = HumanAddr::from("Demeter"); + let token_id2 = "grow2".to_string(); + let ceres = HumanAddr::from("Ceres"); + let token_id3 = "sing".to_string(); + + let mint_msg = HandleMsg::Mint(MintMsg { + token_id: token_id1.clone(), + owner: demeter.clone(), + name: "Growing power".to_string(), + description: Some("Allows the owner the power to grow anything".to_string()), + image: None, + }); + handle(&mut deps, mock_env(), minter.clone(), mint_msg).unwrap(); + + let mint_msg = HandleMsg::Mint(MintMsg { + token_id: token_id2.clone(), + owner: ceres.clone(), + name: "More growing power".to_string(), + description: Some( + "Allows the owner the power to grow anything even faster".to_string(), + ), + image: None, + }); + handle(&mut deps, mock_env(), minter.clone(), mint_msg).unwrap(); + + let mint_msg = HandleMsg::Mint(MintMsg { + token_id: token_id3.clone(), + owner: demeter.clone(), + name: "Sing a lullaby".to_string(), + description: Some("Calm even the most excited children".to_string()), + image: None, + }); + handle(&mut deps, mock_env(), minter.clone(), mint_msg).unwrap(); + + // get all tokens in order: + let expected = vec![token_id1.clone(), token_id2.clone(), token_id3.clone()]; + let tokens = query_all_tokens(&deps, None, None).unwrap(); + assert_eq!(&expected, &tokens.tokens); + // paginate + let tokens = query_all_tokens(&deps, None, Some(2)).unwrap(); + assert_eq!(&expected[..2], &tokens.tokens[..]); + let tokens = query_all_tokens(&deps, Some(expected[1].clone()), None).unwrap(); + assert_eq!(&expected[2..], &tokens.tokens[..]); + + // get by owner + let by_ceres = vec![token_id2.clone()]; + let by_demeter = vec![token_id1.clone(), token_id3.clone()]; + // all tokens by owner + let tokens = query_tokens(&deps, demeter.clone(), None, None).unwrap(); + assert_eq!(&by_demeter, &tokens.tokens); + let tokens = query_tokens(&deps, ceres.clone(), None, None).unwrap(); + assert_eq!(&by_ceres, &tokens.tokens); + + // paginate for demeter + let tokens = query_tokens(&deps, demeter.clone(), None, Some(1)).unwrap(); + assert_eq!(&by_demeter[..1], &tokens.tokens[..]); + let tokens = + query_tokens(&deps, demeter.clone(), Some(by_demeter[0].clone()), Some(3)).unwrap(); + assert_eq!(&by_demeter[1..], &tokens.tokens[..]); + } }