Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TokensByOwner for cw721-base #122

Merged
merged 12 commits into from
Oct 16, 2020
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion contracts/cw721-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" , features = ["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" }
Expand Down
34 changes: 34 additions & 0 deletions contracts/cw721-base/schema/query_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
153 changes: 125 additions & 28 deletions contracts/cw721-base/src/contract.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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::{calc_range_start_human, calc_range_start_string};
use cw0::maybe_canonical;
use cw2::set_contract_version;
use cw721::{
AllNftInfoResponse, ApprovedForAllResponse, ContractInfoResponse, Expiration, NftInfoResponse,
Expand All @@ -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, tokens, Approval, TokenInfo, CONTRACT_INFO, MINTER, OPERATORS,
};
use cw_storage_plus::Bound;

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw721-base";
Expand All @@ -33,9 +33,9 @@ pub fn init<S: Storage, A: Api, Q: Querier>(
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())
}

Expand Down Expand Up @@ -77,7 +77,7 @@ pub fn handle_mint<S: Storage, A: Api, Q: Querier>(
info: MessageInfo,
msg: MintMsg,
) -> Result<HandleResponse, ContractError> {
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 {
Expand All @@ -92,7 +92,7 @@ pub fn handle_mint<S: Storage, A: Api, Q: Querier>(
description: msg.description.unwrap_or_default(),
image: msg.image,
};
tokens(&mut deps.storage).update(msg.token_id.as_bytes(), |old| match old {
tokens().update(&mut deps.storage, &msg.token_id, |old| match old {
Some(_) => Err(ContractError::Claimed {}),
None => Ok(token),
})?;
Expand Down Expand Up @@ -168,13 +168,13 @@ pub fn _transfer_nft<S: Storage, A: Api, Q: Querier>(
recipient: &HumanAddr,
token_id: &str,
) -> Result<TokenInfo, ContractError> {
let mut token = tokens(&mut deps.storage).load(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(&mut deps.storage).save(token_id.as_bytes(), &token)?;
tokens().save(&mut deps.storage, &token_id, &token)?;
Ok(token)
}

Expand Down Expand Up @@ -231,7 +231,7 @@ pub fn _update_approvals<S: Storage, A: Api, Q: Querier>(
add: bool,
expires: Option<Expiration>,
) -> Result<TokenInfo, ContractError> {
let mut token = tokens(&mut deps.storage).load(token_id.as_bytes())?;
let mut token = tokens().load(&deps.storage, &token_id)?;
// ensure we have permissions
check_can_approve(&deps, env, info, &token)?;

Expand All @@ -257,7 +257,7 @@ pub fn _update_approvals<S: Storage, A: Api, Q: Querier>(
token.approvals.push(approval);
}

tokens(&mut deps.storage).save(token_id.as_bytes(), &token)?;
tokens().save(&mut deps.storage, &token_id, &token)?;

Ok(token)
}
Expand All @@ -278,7 +278,7 @@ pub fn handle_approve_all<S: Storage, A: Api, Q: Querier>(
// 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![],
Expand All @@ -299,7 +299,7 @@ pub fn handle_revoke_all<S: Storage, A: Api, Q: Querier>(
) -> Result<HandleResponse, ContractError> {
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![],
Expand All @@ -325,7 +325,7 @@ fn check_can_approve<S: Storage, A: Api, Q: Querier>(
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) {
Expand Down Expand Up @@ -361,7 +361,7 @@ fn check_can_send<S: Storage, A: Api, Q: Querier>(
}

// 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) {
Expand Down Expand Up @@ -415,6 +415,11 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
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)?)
}
Expand All @@ -424,15 +429,15 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
fn query_minter<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
) -> StdResult<MinterResponse> {
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 })
}

fn query_contract_info<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
) -> StdResult<ContractInfoResponse> {
contract_info_read(&deps.storage).load()
CONTRACT_INFO.load(&deps.storage)
}

fn query_num_tokens<S: Storage, A: Api, Q: Querier>(
Expand All @@ -446,7 +451,7 @@ fn query_nft_info<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
token_id: String,
) -> StdResult<NftInfoResponse> {
let info = tokens_read(&deps.storage).load(token_id.as_bytes())?;
let info = tokens().load(&deps.storage, &token_id)?;
Ok(NftInfoResponse {
name: info.name,
description: info.description,
Expand All @@ -460,7 +465,7 @@ fn query_owner_of<S: Storage, A: Api, Q: Querier>(
token_id: String,
include_expired: bool,
) -> StdResult<OwnerOfResponse> {
let info = tokens_read(&deps.storage).load(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)?,
Expand All @@ -478,12 +483,14 @@ fn query_all_approvals<S: Storage, A: Api, Q: Querier>(
start_after: Option<HumanAddr>,
limit: Option<u32>,
) -> StdResult<ApprovedForAllResponse> {
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 = start_canon.map(Bound::exclusive);

let res: StdResult<Vec<_>> = operators_read(&deps.storage, &owner_raw)
.range(start.as_deref(), None, Order::Ascending)
let owner_raw = deps.api.canonical_address(&owner)?;
let res: StdResult<Vec<_>> = OPERATORS
.prefix(&owner_raw)
.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))
Expand All @@ -498,16 +505,37 @@ fn parse_approval<A: Api>(api: A, item: StdResult<KV<Expiration>>) -> StdResult<
})
}

fn query_tokens<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
owner: HumanAddr,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);

let owner_raw = deps.api.canonical_address(&owner)?;
let tokens: Result<Vec<String>, _> = tokens::<S>()
.idx
.owner
.pks(&deps.storage, &owner_raw, start, None, Order::Ascending)
.take(limit)
.map(String::from_utf8)
.collect();
let tokens = tokens.map_err(StdError::invalid_utf8)?;
Ok(TokensResponse { tokens })
}

fn query_all_tokens<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<TokensResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = calc_range_start_string(start_after);
let start = start_after.map(Bound::exclusive);

let tokens: StdResult<Vec<String>> = tokens_read(&deps.storage)
.range(start.as_deref(), None, Order::Ascending)
let tokens: StdResult<Vec<String>> = tokens::<S>()
.range(&deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| item.map(|(k, _)| String::from_utf8_lossy(&k).to_string()))
.collect();
Expand All @@ -520,7 +548,7 @@ fn query_all_nft_info<S: Storage, A: Api, Q: Querier>(
token_id: String,
include_expired: bool,
) -> StdResult<AllNftInfoResponse> {
let info = tokens_read(&deps.storage).load(token_id.as_bytes())?;
let info = tokens().load(&deps.storage, &token_id)?;
Ok(AllNftInfoResponse {
access: OwnerOfResponse {
owner: deps.api.human_address(&info.owner)?,
Expand Down Expand Up @@ -1072,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[..]);
}
}
8 changes: 8 additions & 0 deletions contracts/cw721-base/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ pub enum QueryMsg {
include_expired: Option<bool>,
},

/// With Enumerable extension.
/// Returns all tokens owned by the given address, [] if unset.
/// Return type: TokensResponse.
Tokens {
owner: HumanAddr,
start_after: Option<String>,
limit: Option<u32>,
},
/// With Enumerable extension.
/// Requires pagination. Lists all token_ids controlled by the contract.
/// Return type: TokensResponse.
Expand Down
Loading