From 965c8b9499047e5a1ae10529e1e495a4c177ed09 Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Tue, 20 Sep 2022 02:52:28 -0500 Subject: [PATCH 1/2] Use public queries and move tests --- contracts/cw1155-base/src/contract.rs | 824 +++----------------------- contracts/cw1155-base/src/lib.rs | 3 + contracts/cw1155-base/src/tests.rs | 694 ++++++++++++++++++++++ 3 files changed, 780 insertions(+), 741 deletions(-) create mode 100644 contracts/cw1155-base/src/tests.rs diff --git a/contracts/cw1155-base/src/contract.rs b/contracts/cw1155-base/src/contract.rs index 5add6b405..425609119 100644 --- a/contracts/cw1155-base/src/contract.rs +++ b/contracts/cw1155-base/src/contract.rs @@ -1,22 +1,21 @@ -use cosmwasm_std::entry_point; +use crate::{ + error::ContractError, + msg::InstantiateMsg, + state::{APPROVES, BALANCES, MINTER, TOKENS}, +}; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, SubMsg, - Uint128, + entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdResult, SubMsg, Uint128, }; -use cw_storage_plus::Bound; - use cw1155::{ ApproveAllEvent, ApprovedForAllResponse, BalanceResponse, BatchBalanceResponse, Cw1155BatchReceiveMsg, Cw1155ExecuteMsg, Cw1155QueryMsg, Cw1155ReceiveMsg, Expiration, IsApprovedForAllResponse, TokenId, TokenInfoResponse, TokensResponse, TransferEvent, }; use cw2::set_contract_version; +use cw_storage_plus::Bound; use cw_utils::{maybe_addr, Event}; -use crate::error::ContractError; -use crate::msg::InstantiateMsg; -use crate::state::{APPROVES, BALANCES, MINTER, TOKENS}; - // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw1155-base"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -419,65 +418,68 @@ pub fn execute_revoke_all(env: ExecuteEnv, operator: String) -> Result StdResult { match msg { Cw1155QueryMsg::Balance { owner, token_id } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let balance = BALANCES - .may_load(deps.storage, (&owner_addr, &token_id))? - .unwrap_or_default(); - to_binary(&BalanceResponse { balance }) + to_binary(&query_balance(deps, owner, token_id)?) } Cw1155QueryMsg::BatchBalance { owner, token_ids } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let balances = token_ids - .into_iter() - .map(|token_id| -> StdResult<_> { - Ok(BALANCES - .may_load(deps.storage, (&owner_addr, &token_id))? - .unwrap_or_default()) - }) - .collect::>()?; - to_binary(&BatchBalanceResponse { balances }) + to_binary(&query_batch_balance(deps, owner, token_ids)?) } Cw1155QueryMsg::IsApprovedForAll { owner, operator } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let operator_addr = deps.api.addr_validate(&operator)?; - let approved = check_can_approve(deps, &env, &owner_addr, &operator_addr)?; - to_binary(&IsApprovedForAllResponse { approved }) + to_binary(&query_is_approved_for_all(deps, env, owner, operator)?) } Cw1155QueryMsg::ApprovedForAll { owner, include_expired, start_after, limit, - } => { - let owner_addr = deps.api.addr_validate(&owner)?; - let start_addr = maybe_addr(deps.api, start_after)?; - to_binary(&query_all_approvals( - deps, - env, - owner_addr, - include_expired.unwrap_or(false), - start_addr, - limit, - )?) - } - Cw1155QueryMsg::TokenInfo { token_id } => { - let url = TOKENS.load(deps.storage, &token_id)?; - to_binary(&TokenInfoResponse { url }) - } + } => to_binary(&query_approved_for_all( + deps, + env, + owner, + include_expired.unwrap_or(false), + start_after, + limit, + )?), + Cw1155QueryMsg::TokenInfo { token_id } => to_binary(&query_token_info(deps, token_id)?), Cw1155QueryMsg::Tokens { owner, start_after, limit, - } => { - let owner_addr = deps.api.addr_validate(&owner)?; - to_binary(&query_tokens(deps, owner_addr, start_after, limit)?) - } + } => to_binary(&query_tokens(deps, owner, start_after, limit)?), Cw1155QueryMsg::AllTokens { start_after, limit } => { to_binary(&query_all_tokens(deps, start_after, limit)?) } } } +pub fn query_balance(deps: Deps, owner: String, token_id: String) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let balance = BALANCES + .may_load(deps.storage, (&owner, &token_id))? + .unwrap_or_default(); + + Ok(BalanceResponse { balance }) +} + +pub fn query_batch_balance( + deps: Deps, + owner: String, + token_ids: Vec, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let balances = token_ids + .into_iter() + .map(|token_id| -> StdResult<_> { + Ok(BALANCES + .may_load(deps.storage, (&owner, &token_id))? + .unwrap_or_default()) + }) + .collect::>()?; + + Ok(BatchBalanceResponse { balances }) +} + fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { item.map(|(addr, expires)| cw1155::Approval { spender: addr.into(), @@ -485,14 +487,16 @@ fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult, + start_after: Option, limit: Option, ) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + let start_after = maybe_addr(deps.api, start_after)?; let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after.as_ref().map(Bound::exclusive); @@ -503,15 +507,37 @@ fn query_all_approvals( .take(limit) .map(build_approval) .collect::>()?; + Ok(ApprovedForAllResponse { operators }) } -fn query_tokens( +pub fn query_token_info(deps: Deps, token_id: String) -> StdResult { + let url = TOKENS.load(deps.storage, &token_id)?; + + Ok(TokenInfoResponse { url }) +} + +pub fn query_is_approved_for_all( deps: Deps, - owner: Addr, + env: Env, + owner: String, + operator: String, +) -> StdResult { + let owner_addr = deps.api.addr_validate(&owner)?; + let operator_addr = deps.api.addr_validate(&operator)?; + + let approved = check_can_approve(deps, &env, &owner_addr, &operator_addr)?; + + Ok(IsApprovedForAllResponse { approved }) +} + +pub fn query_tokens( + deps: Deps, + owner: String, start_after: Option, limit: Option, ) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); @@ -520,706 +546,22 @@ fn query_tokens( .keys(deps.storage, start, None, Order::Ascending) .take(limit) .collect::>()?; + Ok(TokensResponse { tokens }) } -fn query_all_tokens( +pub fn query_all_tokens( deps: Deps, start_after: Option, limit: Option, ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + let tokens = TOKENS .keys(deps.storage, start, None, Order::Ascending) .take(limit) .collect::>()?; - Ok(TokensResponse { tokens }) -} - -#[cfg(test)] -mod tests { - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{OverflowError, StdError}; - - use super::*; - - #[test] - fn check_transfers() { - // A long test case that try to cover as many cases as possible. - // Summary of what it does: - // - try mint without permission, fail - // - mint with permission, success - // - query balance of receipant, success - // - try transfer without approval, fail - // - approve - // - transfer again, success - // - query balance of transfer participants - // - batch mint token2 and token3, success - // - try batch transfer without approval, fail - // - approve and try batch transfer again, success - // - batch query balances - // - user1 revoke approval to minter - // - query approval status - // - minter try to transfer, fail - // - user1 burn token1 - // - user1 batch burn token2 and token3 - let token1 = "token1".to_owned(); - let token2 = "token2".to_owned(); - let token3 = "token3".to_owned(); - let minter = String::from("minter"); - let user1 = String::from("user1"); - let user2 = String::from("user2"); - - let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - minter: minter.clone(), - }; - let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // invalid mint, user1 don't mint permission - let mint_msg = Cw1155ExecuteMsg::Mint { - to: user1.clone(), - token_id: token1.clone(), - value: 1u64.into(), - msg: None, - }; - assert!(matches!( - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - mint_msg.clone(), - ), - Err(ContractError::Unauthorized {}) - )); - - // valid mint - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - mint_msg, - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token1) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("to", &user1) - ); - - // query balance - assert_eq!( - to_binary(&BalanceResponse { - balance: 1u64.into() - }), - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::Balance { - owner: user1.clone(), - token_id: token1.clone(), - } - ), - ); - - let transfer_msg = Cw1155ExecuteMsg::SendFrom { - from: user1.clone(), - to: user2.clone(), - token_id: token1.clone(), - value: 1u64.into(), - msg: None, - }; - - // not approved yet - assert!(matches!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - transfer_msg.clone(), - ), - Err(ContractError::Unauthorized {}) - )); - - // approve - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::ApproveAll { - operator: minter.clone(), - expires: None, - }, - ) - .unwrap(); - - // transfer - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - transfer_msg, - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token1) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user1) - .add_attribute("to", &user2) - ); - - // query balance - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::Balance { - owner: user2.clone(), - token_id: token1.clone(), - } - ), - to_binary(&BalanceResponse { - balance: 1u64.into() - }), - ); - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::Balance { - owner: user1.clone(), - token_id: token1.clone(), - } - ), - to_binary(&BalanceResponse { - balance: 0u64.into() - }), - ); - - // batch mint token2 and token3 - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::BatchMint { - to: user2.clone(), - batch: vec![(token2.clone(), 1u64.into()), (token3.clone(), 1u64.into())], - msg: None - }, - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token2) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("to", &user2) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token3) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("to", &user2) - ); - - // invalid batch transfer, (user2 not approved yet) - let batch_transfer_msg = Cw1155ExecuteMsg::BatchSendFrom { - from: user2.clone(), - to: user1.clone(), - batch: vec![ - (token1.clone(), 1u64.into()), - (token2.clone(), 1u64.into()), - (token3.clone(), 1u64.into()), - ], - msg: None, - }; - assert!(matches!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - batch_transfer_msg.clone(), - ), - Err(ContractError::Unauthorized {}), - )); - - // user2 approve - execute( - deps.as_mut(), - mock_env(), - mock_info(user2.as_ref(), &[]), - Cw1155ExecuteMsg::ApproveAll { - operator: minter.clone(), - expires: None, - }, - ) - .unwrap(); - - // valid batch transfer - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - batch_transfer_msg, - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token1) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user2) - .add_attribute("to", &user1) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token2) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user2) - .add_attribute("to", &user1) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token3) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user2) - .add_attribute("to", &user1) - ); - - // batch query - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::BatchBalance { - owner: user1.clone(), - token_ids: vec![token1.clone(), token2.clone(), token3.clone()], - } - ), - to_binary(&BatchBalanceResponse { - balances: vec![1u64.into(), 1u64.into(), 1u64.into()] - }), - ); - - // user1 revoke approval - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::RevokeAll { - operator: minter.clone(), - }, - ) - .unwrap(); - - // query approval status - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::IsApprovedForAll { - owner: user1.clone(), - operator: minter.clone(), - } - ), - to_binary(&IsApprovedForAllResponse { approved: false }), - ); - - // tranfer without approval - assert!(matches!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::SendFrom { - from: user1.clone(), - to: user2, - token_id: token1.clone(), - value: 1u64.into(), - msg: None, - }, - ), - Err(ContractError::Unauthorized {}) - )); - - // burn token1 - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::Burn { - from: user1.clone(), - token_id: token1.clone(), - value: 1u64.into(), - } - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token1) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user1) - ); - - // burn them all - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::BatchBurn { - from: user1.clone(), - batch: vec![(token2.clone(), 1u64.into()), (token3.clone(), 1u64.into())] - } - ) - .unwrap(), - Response::new() - .add_attribute("action", "transfer") - .add_attribute("token_id", &token2) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user1) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token3) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user1) - ); - } - - #[test] - fn check_send_contract() { - let receiver = String::from("receive_contract"); - let minter = String::from("minter"); - let user1 = String::from("user1"); - let token1 = "token1".to_owned(); - let token2 = "token2".to_owned(); - let dummy_msg = Binary::default(); - - let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - minter: minter.clone(), - }; - let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::Mint { - to: user1.clone(), - token_id: token2.clone(), - value: 1u64.into(), - msg: None, - }, - ) - .unwrap(); - - // mint to contract - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::Mint { - to: receiver.clone(), - token_id: token1.clone(), - value: 1u64.into(), - msg: Some(dummy_msg.clone()), - }, - ) - .unwrap(), - Response::new() - .add_message( - Cw1155ReceiveMsg { - operator: minter.clone(), - from: None, - amount: 1u64.into(), - token_id: token1.clone(), - msg: dummy_msg.clone(), - } - .into_cosmos_msg(receiver.clone()) - .unwrap() - ) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token1) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("to", &receiver) - ); - - // BatchSendFrom - assert_eq!( - execute( - deps.as_mut(), - mock_env(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::BatchSendFrom { - from: user1.clone(), - to: receiver.clone(), - batch: vec![(token2.clone(), 1u64.into())], - msg: Some(dummy_msg.clone()), - }, - ) - .unwrap(), - Response::new() - .add_message( - Cw1155BatchReceiveMsg { - operator: user1.clone(), - from: Some(user1.clone()), - batch: vec![(token2.clone(), 1u64.into())], - msg: dummy_msg, - } - .into_cosmos_msg(receiver.clone()) - .unwrap() - ) - .add_attribute("action", "transfer") - .add_attribute("token_id", &token2) - .add_attribute("amount", 1u64.to_string()) - .add_attribute("from", &user1) - .add_attribute("to", &receiver) - ); - } - - #[test] - fn check_queries() { - // mint multiple types of tokens, and query them - // grant approval to multiple operators, and query them - let tokens = (0..10).map(|i| format!("token{}", i)).collect::>(); - let users = (0..10).map(|i| format!("user{}", i)).collect::>(); - let minter = String::from("minter"); - - let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - minter: minter.clone(), - }; - let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - execute( - deps.as_mut(), - mock_env(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::BatchMint { - to: users[0].clone(), - batch: tokens - .iter() - .map(|token_id| (token_id.clone(), 1u64.into())) - .collect::>(), - msg: None, - }, - ) - .unwrap(); - - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::Tokens { - owner: users[0].clone(), - start_after: None, - limit: Some(5), - }, - ), - to_binary(&TokensResponse { - tokens: tokens[..5].to_owned() - }) - ); - - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::Tokens { - owner: users[0].clone(), - start_after: Some("token5".to_owned()), - limit: Some(5), - }, - ), - to_binary(&TokensResponse { - tokens: tokens[6..].to_owned() - }) - ); - - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::AllTokens { - start_after: Some("token5".to_owned()), - limit: Some(5), - }, - ), - to_binary(&TokensResponse { - tokens: tokens[6..].to_owned() - }) - ); - - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::TokenInfo { - token_id: "token5".to_owned() - }, - ), - to_binary(&TokenInfoResponse { url: "".to_owned() }) - ); - - for user in users[1..].iter() { - execute( - deps.as_mut(), - mock_env(), - mock_info(users[0].as_ref(), &[]), - Cw1155ExecuteMsg::ApproveAll { - operator: user.clone(), - expires: None, - }, - ) - .unwrap(); - } - - assert_eq!( - query( - deps.as_ref(), - mock_env(), - Cw1155QueryMsg::ApprovedForAll { - owner: users[0].clone(), - include_expired: None, - start_after: Some(String::from("user2")), - limit: Some(1), - }, - ), - to_binary(&ApprovedForAllResponse { - operators: vec![cw1155::Approval { - spender: users[3].clone(), - expires: Expiration::Never {} - }], - }) - ); - } - #[test] - fn approval_expires() { - let mut deps = mock_dependencies(); - let token1 = "token1".to_owned(); - let minter = String::from("minter"); - let user1 = String::from("user1"); - let user2 = String::from("user2"); - - let env = { - let mut env = mock_env(); - env.block.height = 10; - env - }; - - let msg = InstantiateMsg { - minter: minter.clone(), - }; - let res = instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - execute( - deps.as_mut(), - env.clone(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::Mint { - to: user1.clone(), - token_id: token1, - value: 1u64.into(), - msg: None, - }, - ) - .unwrap(); - - // invalid expires should be rejected - assert!(matches!( - execute( - deps.as_mut(), - env.clone(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::ApproveAll { - operator: user2.clone(), - expires: Some(Expiration::AtHeight(5)), - }, - ), - Err(_) - )); - - execute( - deps.as_mut(), - env.clone(), - mock_info(user1.as_ref(), &[]), - Cw1155ExecuteMsg::ApproveAll { - operator: user2.clone(), - expires: Some(Expiration::AtHeight(100)), - }, - ) - .unwrap(); - - let query_msg = Cw1155QueryMsg::IsApprovedForAll { - owner: user1, - operator: user2, - }; - assert_eq!( - query(deps.as_ref(), env, query_msg.clone()), - to_binary(&IsApprovedForAllResponse { approved: true }) - ); - - let env = { - let mut env = mock_env(); - env.block.height = 100; - env - }; - - assert_eq!( - query(deps.as_ref(), env, query_msg,), - to_binary(&IsApprovedForAllResponse { approved: false }) - ); - } - - #[test] - fn mint_overflow() { - let mut deps = mock_dependencies(); - let token1 = "token1".to_owned(); - let minter = String::from("minter"); - let user1 = String::from("user1"); - - let env = mock_env(); - let msg = InstantiateMsg { - minter: minter.clone(), - }; - let res = instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg).unwrap(); - assert_eq!(0, res.messages.len()); - - execute( - deps.as_mut(), - env.clone(), - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::Mint { - to: user1.clone(), - token_id: token1.clone(), - value: u128::MAX.into(), - msg: None, - }, - ) - .unwrap(); - - assert!(matches!( - execute( - deps.as_mut(), - env, - mock_info(minter.as_ref(), &[]), - Cw1155ExecuteMsg::Mint { - to: user1, - token_id: token1, - value: 1u64.into(), - msg: None, - }, - ), - Err(ContractError::Std(StdError::Overflow { - source: OverflowError { .. }, - .. - })) - )); - } + Ok(TokensResponse { tokens }) } diff --git a/contracts/cw1155-base/src/lib.rs b/contracts/cw1155-base/src/lib.rs index dfedc9dc6..69b809fe1 100644 --- a/contracts/cw1155-base/src/lib.rs +++ b/contracts/cw1155-base/src/lib.rs @@ -4,3 +4,6 @@ pub mod msg; pub mod state; pub use crate::error::ContractError; + +#[cfg(test)] +mod tests; diff --git a/contracts/cw1155-base/src/tests.rs b/contracts/cw1155-base/src/tests.rs new file mode 100644 index 000000000..6202bdef8 --- /dev/null +++ b/contracts/cw1155-base/src/tests.rs @@ -0,0 +1,694 @@ +use crate::{ + contract::{execute, instantiate, query}, + msg::InstantiateMsg, + ContractError, +}; +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, Binary, OverflowError, Response, StdError, +}; +use cw1155::{ + ApprovedForAllResponse, BalanceResponse, BatchBalanceResponse, Cw1155BatchReceiveMsg, + Cw1155ExecuteMsg, Cw1155QueryMsg, Cw1155ReceiveMsg, IsApprovedForAllResponse, + TokenInfoResponse, TokensResponse, +}; +use cw_utils::Expiration; + +#[test] +fn check_transfers() { + // A long test case that try to cover as many cases as possible. + // Summary of what it does: + // - try mint without permission, fail + // - mint with permission, success + // - query balance of receipant, success + // - try transfer without approval, fail + // - approve + // - transfer again, success + // - query balance of transfer participants + // - batch mint token2 and token3, success + // - try batch transfer without approval, fail + // - approve and try batch transfer again, success + // - batch query balances + // - user1 revoke approval to minter + // - query approval status + // - minter try to transfer, fail + // - user1 burn token1 + // - user1 batch burn token2 and token3 + let token1 = "token1".to_owned(); + let token2 = "token2".to_owned(); + let token3 = "token3".to_owned(); + let minter = String::from("minter"); + let user1 = String::from("user1"); + let user2 = String::from("user2"); + + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + minter: minter.clone(), + }; + let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // invalid mint, user1 don't mint permission + let mint_msg = Cw1155ExecuteMsg::Mint { + to: user1.clone(), + token_id: token1.clone(), + value: 1u64.into(), + msg: None, + }; + assert!(matches!( + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + mint_msg.clone(), + ), + Err(ContractError::Unauthorized {}) + )); + + // valid mint + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + mint_msg, + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token1) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("to", &user1) + ); + + // query balance + assert_eq!( + to_binary(&BalanceResponse { + balance: 1u64.into() + }), + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::Balance { + owner: user1.clone(), + token_id: token1.clone(), + } + ), + ); + + let transfer_msg = Cw1155ExecuteMsg::SendFrom { + from: user1.clone(), + to: user2.clone(), + token_id: token1.clone(), + value: 1u64.into(), + msg: None, + }; + + // not approved yet + assert!(matches!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + transfer_msg.clone(), + ), + Err(ContractError::Unauthorized {}) + )); + + // approve + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::ApproveAll { + operator: minter.clone(), + expires: None, + }, + ) + .unwrap(); + + // transfer + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + transfer_msg, + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token1) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user1) + .add_attribute("to", &user2) + ); + + // query balance + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::Balance { + owner: user2.clone(), + token_id: token1.clone(), + } + ), + to_binary(&BalanceResponse { + balance: 1u64.into() + }), + ); + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::Balance { + owner: user1.clone(), + token_id: token1.clone(), + } + ), + to_binary(&BalanceResponse { + balance: 0u64.into() + }), + ); + + // batch mint token2 and token3 + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::BatchMint { + to: user2.clone(), + batch: vec![(token2.clone(), 1u64.into()), (token3.clone(), 1u64.into())], + msg: None + }, + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token2) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("to", &user2) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token3) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("to", &user2) + ); + + // invalid batch transfer, (user2 not approved yet) + let batch_transfer_msg = Cw1155ExecuteMsg::BatchSendFrom { + from: user2.clone(), + to: user1.clone(), + batch: vec![ + (token1.clone(), 1u64.into()), + (token2.clone(), 1u64.into()), + (token3.clone(), 1u64.into()), + ], + msg: None, + }; + assert!(matches!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + batch_transfer_msg.clone(), + ), + Err(ContractError::Unauthorized {}), + )); + + // user2 approve + execute( + deps.as_mut(), + mock_env(), + mock_info(user2.as_ref(), &[]), + Cw1155ExecuteMsg::ApproveAll { + operator: minter.clone(), + expires: None, + }, + ) + .unwrap(); + + // valid batch transfer + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + batch_transfer_msg, + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token1) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user2) + .add_attribute("to", &user1) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token2) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user2) + .add_attribute("to", &user1) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token3) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user2) + .add_attribute("to", &user1) + ); + + // batch query + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::BatchBalance { + owner: user1.clone(), + token_ids: vec![token1.clone(), token2.clone(), token3.clone()], + } + ), + to_binary(&BatchBalanceResponse { + balances: vec![1u64.into(), 1u64.into(), 1u64.into()] + }), + ); + + // user1 revoke approval + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::RevokeAll { + operator: minter.clone(), + }, + ) + .unwrap(); + + // query approval status + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::IsApprovedForAll { + owner: user1.clone(), + operator: minter.clone(), + } + ), + to_binary(&IsApprovedForAllResponse { approved: false }), + ); + + // tranfer without approval + assert!(matches!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::SendFrom { + from: user1.clone(), + to: user2, + token_id: token1.clone(), + value: 1u64.into(), + msg: None, + }, + ), + Err(ContractError::Unauthorized {}) + )); + + // burn token1 + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::Burn { + from: user1.clone(), + token_id: token1.clone(), + value: 1u64.into(), + } + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token1) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user1) + ); + + // burn them all + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::BatchBurn { + from: user1.clone(), + batch: vec![(token2.clone(), 1u64.into()), (token3.clone(), 1u64.into())] + } + ) + .unwrap(), + Response::new() + .add_attribute("action", "transfer") + .add_attribute("token_id", &token2) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user1) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token3) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user1) + ); +} + +#[test] +fn check_send_contract() { + let receiver = String::from("receive_contract"); + let minter = String::from("minter"); + let user1 = String::from("user1"); + let token1 = "token1".to_owned(); + let token2 = "token2".to_owned(); + let dummy_msg = Binary::default(); + + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + minter: minter.clone(), + }; + let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::Mint { + to: user1.clone(), + token_id: token2.clone(), + value: 1u64.into(), + msg: None, + }, + ) + .unwrap(); + + // mint to contract + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::Mint { + to: receiver.clone(), + token_id: token1.clone(), + value: 1u64.into(), + msg: Some(dummy_msg.clone()), + }, + ) + .unwrap(), + Response::new() + .add_message( + Cw1155ReceiveMsg { + operator: minter.clone(), + from: None, + amount: 1u64.into(), + token_id: token1.clone(), + msg: dummy_msg.clone(), + } + .into_cosmos_msg(receiver.clone()) + .unwrap() + ) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token1) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("to", &receiver) + ); + + // BatchSendFrom + assert_eq!( + execute( + deps.as_mut(), + mock_env(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::BatchSendFrom { + from: user1.clone(), + to: receiver.clone(), + batch: vec![(token2.clone(), 1u64.into())], + msg: Some(dummy_msg.clone()), + }, + ) + .unwrap(), + Response::new() + .add_message( + Cw1155BatchReceiveMsg { + operator: user1.clone(), + from: Some(user1.clone()), + batch: vec![(token2.clone(), 1u64.into())], + msg: dummy_msg, + } + .into_cosmos_msg(receiver.clone()) + .unwrap() + ) + .add_attribute("action", "transfer") + .add_attribute("token_id", &token2) + .add_attribute("amount", 1u64.to_string()) + .add_attribute("from", &user1) + .add_attribute("to", &receiver) + ); +} + +#[test] +fn check_queries() { + // mint multiple types of tokens, and query them + // grant approval to multiple operators, and query them + let tokens = (0..10).map(|i| format!("token{}", i)).collect::>(); + let users = (0..10).map(|i| format!("user{}", i)).collect::>(); + let minter = String::from("minter"); + + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + minter: minter.clone(), + }; + let res = instantiate(deps.as_mut(), mock_env(), mock_info("operator", &[]), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + execute( + deps.as_mut(), + mock_env(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::BatchMint { + to: users[0].clone(), + batch: tokens + .iter() + .map(|token_id| (token_id.clone(), 1u64.into())) + .collect::>(), + msg: None, + }, + ) + .unwrap(); + + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::Tokens { + owner: users[0].clone(), + start_after: None, + limit: Some(5), + }, + ), + to_binary(&TokensResponse { + tokens: tokens[..5].to_owned() + }) + ); + + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::Tokens { + owner: users[0].clone(), + start_after: Some("token5".to_owned()), + limit: Some(5), + }, + ), + to_binary(&TokensResponse { + tokens: tokens[6..].to_owned() + }) + ); + + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::AllTokens { + start_after: Some("token5".to_owned()), + limit: Some(5), + }, + ), + to_binary(&TokensResponse { + tokens: tokens[6..].to_owned() + }) + ); + + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::TokenInfo { + token_id: "token5".to_owned() + }, + ), + to_binary(&TokenInfoResponse { url: "".to_owned() }) + ); + + for user in users[1..].iter() { + execute( + deps.as_mut(), + mock_env(), + mock_info(users[0].as_ref(), &[]), + Cw1155ExecuteMsg::ApproveAll { + operator: user.clone(), + expires: None, + }, + ) + .unwrap(); + } + + assert_eq!( + query( + deps.as_ref(), + mock_env(), + Cw1155QueryMsg::ApprovedForAll { + owner: users[0].clone(), + include_expired: None, + start_after: Some(String::from("user2")), + limit: Some(1), + }, + ), + to_binary(&ApprovedForAllResponse { + operators: vec![cw1155::Approval { + spender: users[3].clone(), + expires: Expiration::Never {} + }], + }) + ); +} + +#[test] +fn approval_expires() { + let mut deps = mock_dependencies(); + let token1 = "token1".to_owned(); + let minter = String::from("minter"); + let user1 = String::from("user1"); + let user2 = String::from("user2"); + + let env = { + let mut env = mock_env(); + env.block.height = 10; + env + }; + + let msg = InstantiateMsg { + minter: minter.clone(), + }; + let res = instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + execute( + deps.as_mut(), + env.clone(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::Mint { + to: user1.clone(), + token_id: token1, + value: 1u64.into(), + msg: None, + }, + ) + .unwrap(); + + // invalid expires should be rejected + assert!(matches!( + execute( + deps.as_mut(), + env.clone(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::ApproveAll { + operator: user2.clone(), + expires: Some(Expiration::AtHeight(5)), + }, + ), + Err(_) + )); + + execute( + deps.as_mut(), + env.clone(), + mock_info(user1.as_ref(), &[]), + Cw1155ExecuteMsg::ApproveAll { + operator: user2.clone(), + expires: Some(Expiration::AtHeight(100)), + }, + ) + .unwrap(); + + let query_msg = Cw1155QueryMsg::IsApprovedForAll { + owner: user1, + operator: user2, + }; + assert_eq!( + query(deps.as_ref(), env, query_msg.clone()), + to_binary(&IsApprovedForAllResponse { approved: true }) + ); + + let env = { + let mut env = mock_env(); + env.block.height = 100; + env + }; + + assert_eq!( + query(deps.as_ref(), env, query_msg,), + to_binary(&IsApprovedForAllResponse { approved: false }) + ); +} + +#[test] +fn mint_overflow() { + let mut deps = mock_dependencies(); + let token1 = "token1".to_owned(); + let minter = String::from("minter"); + let user1 = String::from("user1"); + + let env = mock_env(); + let msg = InstantiateMsg { + minter: minter.clone(), + }; + let res = instantiate(deps.as_mut(), env.clone(), mock_info("operator", &[]), msg).unwrap(); + assert_eq!(0, res.messages.len()); + + execute( + deps.as_mut(), + env.clone(), + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::Mint { + to: user1.clone(), + token_id: token1.clone(), + value: u128::MAX.into(), + msg: None, + }, + ) + .unwrap(); + + assert!(matches!( + execute( + deps.as_mut(), + env, + mock_info(minter.as_ref(), &[]), + Cw1155ExecuteMsg::Mint { + to: user1, + token_id: token1, + value: 1u64.into(), + msg: None, + }, + ), + Err(ContractError::Std(StdError::Overflow { + source: OverflowError { .. }, + .. + })) + )); +} From bd08192b31f12753d1f669fc689ef8d4e141604d Mon Sep 17 00:00:00 2001 From: Gabriel Lopez Date: Mon, 26 Sep 2022 21:56:34 -0500 Subject: [PATCH 2/2] Moved execute and query logic to modules https://github.com/CosmWasm/cw-plus/pull/804#discussion_r979888833 --- contracts/cw1155-base/src/contract.rs | 494 +------------------------- contracts/cw1155-base/src/execute.rs | 312 ++++++++++++++++ contracts/cw1155-base/src/helpers.rs | 30 ++ contracts/cw1155-base/src/lib.rs | 3 + contracts/cw1155-base/src/query.rs | 130 +++++++ 5 files changed, 493 insertions(+), 476 deletions(-) create mode 100644 contracts/cw1155-base/src/execute.rs create mode 100644 contracts/cw1155-base/src/helpers.rs create mode 100644 contracts/cw1155-base/src/query.rs diff --git a/contracts/cw1155-base/src/contract.rs b/contracts/cw1155-base/src/contract.rs index 425609119..eab6b12e8 100644 --- a/contracts/cw1155-base/src/contract.rs +++ b/contracts/cw1155-base/src/contract.rs @@ -1,28 +1,14 @@ -use crate::{ - error::ContractError, - msg::InstantiateMsg, - state::{APPROVES, BALANCES, MINTER, TOKENS}, -}; +use crate::{error::ContractError, execute, msg::InstantiateMsg, query, state::MINTER}; use cosmwasm_std::{ - entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, - StdResult, SubMsg, Uint128, -}; -use cw1155::{ - ApproveAllEvent, ApprovedForAllResponse, BalanceResponse, BatchBalanceResponse, - Cw1155BatchReceiveMsg, Cw1155ExecuteMsg, Cw1155QueryMsg, Cw1155ReceiveMsg, Expiration, - IsApprovedForAllResponse, TokenId, TokenInfoResponse, TokensResponse, TransferEvent, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; +use cw1155::{Cw1155ExecuteMsg, Cw1155QueryMsg}; use cw2::set_contract_version; -use cw_storage_plus::Bound; -use cw_utils::{maybe_addr, Event}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw1155-base"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 30; - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -58,380 +44,51 @@ pub fn execute( token_id, value, msg, - } => execute_send_from(env, from, to, token_id, value, msg), + } => execute::send_from(env, from, to, token_id, value, msg), Cw1155ExecuteMsg::BatchSendFrom { from, to, batch, msg, - } => execute_batch_send_from(env, from, to, batch, msg), + } => execute::batch_send_from(env, from, to, batch, msg), Cw1155ExecuteMsg::Mint { to, token_id, value, msg, - } => execute_mint(env, to, token_id, value, msg), - Cw1155ExecuteMsg::BatchMint { to, batch, msg } => execute_batch_mint(env, to, batch, msg), + } => execute::mint(env, to, token_id, value, msg), + Cw1155ExecuteMsg::BatchMint { to, batch, msg } => execute::batch_mint(env, to, batch, msg), Cw1155ExecuteMsg::Burn { from, token_id, value, - } => execute_burn(env, from, token_id, value), - Cw1155ExecuteMsg::BatchBurn { from, batch } => execute_batch_burn(env, from, batch), + } => execute::burn(env, from, token_id, value), + Cw1155ExecuteMsg::BatchBurn { from, batch } => execute::batch_burn(env, from, batch), Cw1155ExecuteMsg::ApproveAll { operator, expires } => { - execute_approve_all(env, operator, expires) - } - Cw1155ExecuteMsg::RevokeAll { operator } => execute_revoke_all(env, operator), - } -} - -/// When from is None: mint new coins -/// When to is None: burn coins -/// When both are None: no token balance is changed, pointless but valid -/// -/// Make sure permissions are checked before calling this. -fn execute_transfer_inner<'a>( - deps: &'a mut DepsMut, - from: Option<&'a Addr>, - to: Option<&'a Addr>, - token_id: &'a str, - amount: Uint128, -) -> Result, ContractError> { - if let Some(from_addr) = from { - BALANCES.update( - deps.storage, - (from_addr, token_id), - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_sub(amount)?) - }, - )?; - } - - if let Some(to_addr) = to { - BALANCES.update( - deps.storage, - (to_addr, token_id), - |balance: Option| -> StdResult<_> { - Ok(balance.unwrap_or_default().checked_add(amount)?) - }, - )?; - } - - Ok(TransferEvent { - from: from.map(|x| x.as_ref()), - to: to.map(|x| x.as_ref()), - token_id, - amount, - }) -} - -/// returns true iff the sender can execute approve or reject on the contract -fn check_can_approve(deps: Deps, env: &Env, owner: &Addr, operator: &Addr) -> StdResult { - // owner can approve - if owner == operator { - return Ok(true); - } - // operator can approve - let op = APPROVES.may_load(deps.storage, (owner, operator))?; - Ok(match op { - Some(ex) => !ex.is_expired(&env.block), - None => false, - }) -} - -fn guard_can_approve( - deps: Deps, - env: &Env, - owner: &Addr, - operator: &Addr, -) -> Result<(), ContractError> { - if !check_can_approve(deps, env, owner, operator)? { - Err(ContractError::Unauthorized {}) - } else { - Ok(()) - } -} - -pub fn execute_send_from( - env: ExecuteEnv, - from: String, - to: String, - token_id: TokenId, - amount: Uint128, - msg: Option, -) -> Result { - let from_addr = env.deps.api.addr_validate(&from)?; - let to_addr = env.deps.api.addr_validate(&to)?; - - let ExecuteEnv { - mut deps, - env, - info, - } = env; - - guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; - - let mut rsp = Response::default(); - - let event = execute_transfer_inner( - &mut deps, - Some(&from_addr), - Some(&to_addr), - &token_id, - amount, - )?; - event.add_attributes(&mut rsp); - - if let Some(msg) = msg { - rsp.messages = vec![SubMsg::new( - Cw1155ReceiveMsg { - operator: info.sender.to_string(), - from: Some(from), - amount, - token_id: token_id.clone(), - msg, - } - .into_cosmos_msg(to)?, - )] - } - - Ok(rsp) -} - -pub fn execute_mint( - env: ExecuteEnv, - to: String, - token_id: TokenId, - amount: Uint128, - msg: Option, -) -> Result { - let ExecuteEnv { mut deps, info, .. } = env; - - let to_addr = deps.api.addr_validate(&to)?; - - if info.sender != MINTER.load(deps.storage)? { - return Err(ContractError::Unauthorized {}); - } - - let mut rsp = Response::default(); - - let event = execute_transfer_inner(&mut deps, None, Some(&to_addr), &token_id, amount)?; - event.add_attributes(&mut rsp); - - if let Some(msg) = msg { - rsp.messages = vec![SubMsg::new( - Cw1155ReceiveMsg { - operator: info.sender.to_string(), - from: None, - amount, - token_id: token_id.clone(), - msg, - } - .into_cosmos_msg(to)?, - )] - } - - // insert if not exist - if !TOKENS.has(deps.storage, &token_id) { - // we must save some valid data here - TOKENS.save(deps.storage, &token_id, &String::new())?; - } - - Ok(rsp) -} - -pub fn execute_burn( - env: ExecuteEnv, - from: String, - token_id: TokenId, - amount: Uint128, -) -> Result { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - let from_addr = deps.api.addr_validate(&from)?; - - // whoever can transfer these tokens can burn - guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; - - let mut rsp = Response::default(); - let event = execute_transfer_inner(&mut deps, Some(&from_addr), None, &token_id, amount)?; - event.add_attributes(&mut rsp); - Ok(rsp) -} - -pub fn execute_batch_send_from( - env: ExecuteEnv, - from: String, - to: String, - batch: Vec<(TokenId, Uint128)>, - msg: Option, -) -> Result { - let ExecuteEnv { - mut deps, - env, - info, - } = env; - - let from_addr = deps.api.addr_validate(&from)?; - let to_addr = deps.api.addr_validate(&to)?; - - guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; - - let mut rsp = Response::default(); - for (token_id, amount) in batch.iter() { - let event = execute_transfer_inner( - &mut deps, - Some(&from_addr), - Some(&to_addr), - token_id, - *amount, - )?; - event.add_attributes(&mut rsp); - } - - if let Some(msg) = msg { - rsp.messages = vec![SubMsg::new( - Cw1155BatchReceiveMsg { - operator: info.sender.to_string(), - from: Some(from), - batch, - msg, - } - .into_cosmos_msg(to)?, - )] - }; - - Ok(rsp) -} - -pub fn execute_batch_mint( - env: ExecuteEnv, - to: String, - batch: Vec<(TokenId, Uint128)>, - msg: Option, -) -> Result { - let ExecuteEnv { mut deps, info, .. } = env; - if info.sender != MINTER.load(deps.storage)? { - return Err(ContractError::Unauthorized {}); - } - - let to_addr = deps.api.addr_validate(&to)?; - - let mut rsp = Response::default(); - - for (token_id, amount) in batch.iter() { - let event = execute_transfer_inner(&mut deps, None, Some(&to_addr), token_id, *amount)?; - event.add_attributes(&mut rsp); - - // insert if not exist - if !TOKENS.has(deps.storage, token_id) { - // we must save some valid data here - TOKENS.save(deps.storage, token_id, &String::new())?; + execute::approve_all(env, operator, expires) } + Cw1155ExecuteMsg::RevokeAll { operator } => execute::revoke_all(env, operator), } - - if let Some(msg) = msg { - rsp.messages = vec![SubMsg::new( - Cw1155BatchReceiveMsg { - operator: info.sender.to_string(), - from: None, - batch, - msg, - } - .into_cosmos_msg(to)?, - )] - }; - - Ok(rsp) -} - -pub fn execute_batch_burn( - env: ExecuteEnv, - from: String, - batch: Vec<(TokenId, Uint128)>, -) -> Result { - let ExecuteEnv { - mut deps, - info, - env, - } = env; - - let from_addr = deps.api.addr_validate(&from)?; - - guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; - - let mut rsp = Response::default(); - for (token_id, amount) in batch.into_iter() { - let event = execute_transfer_inner(&mut deps, Some(&from_addr), None, &token_id, amount)?; - event.add_attributes(&mut rsp); - } - Ok(rsp) -} - -pub fn execute_approve_all( - env: ExecuteEnv, - operator: String, - expires: Option, -) -> Result { - let ExecuteEnv { deps, info, env } = env; - - // reject expired data as invalid - let expires = expires.unwrap_or_default(); - if expires.is_expired(&env.block) { - return Err(ContractError::Expired {}); - } - - // set the operator for us - let operator_addr = deps.api.addr_validate(&operator)?; - APPROVES.save(deps.storage, (&info.sender, &operator_addr), &expires)?; - - let mut rsp = Response::default(); - ApproveAllEvent { - sender: info.sender.as_ref(), - operator: &operator, - approved: true, - } - .add_attributes(&mut rsp); - Ok(rsp) -} - -pub fn execute_revoke_all(env: ExecuteEnv, operator: String) -> Result { - let ExecuteEnv { deps, info, .. } = env; - let operator_addr = deps.api.addr_validate(&operator)?; - APPROVES.remove(deps.storage, (&info.sender, &operator_addr)); - - let mut rsp = Response::default(); - ApproveAllEvent { - sender: info.sender.as_ref(), - operator: &operator, - approved: false, - } - .add_attributes(&mut rsp); - Ok(rsp) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: Cw1155QueryMsg) -> StdResult { match msg { Cw1155QueryMsg::Balance { owner, token_id } => { - to_binary(&query_balance(deps, owner, token_id)?) + to_binary(&query::balance(deps, owner, token_id)?) } Cw1155QueryMsg::BatchBalance { owner, token_ids } => { - to_binary(&query_batch_balance(deps, owner, token_ids)?) + to_binary(&query::batch_balance(deps, owner, token_ids)?) } Cw1155QueryMsg::IsApprovedForAll { owner, operator } => { - to_binary(&query_is_approved_for_all(deps, env, owner, operator)?) + to_binary(&query::is_approved_for_all(deps, env, owner, operator)?) } Cw1155QueryMsg::ApprovedForAll { owner, include_expired, start_after, limit, - } => to_binary(&query_approved_for_all( + } => to_binary(&query::approved_for_all( deps, env, owner, @@ -439,129 +96,14 @@ pub fn query(deps: Deps, env: Env, msg: Cw1155QueryMsg) -> StdResult { start_after, limit, )?), - Cw1155QueryMsg::TokenInfo { token_id } => to_binary(&query_token_info(deps, token_id)?), + Cw1155QueryMsg::TokenInfo { token_id } => to_binary(&query::token_info(deps, token_id)?), Cw1155QueryMsg::Tokens { owner, start_after, limit, - } => to_binary(&query_tokens(deps, owner, start_after, limit)?), + } => to_binary(&query::tokens(deps, owner, start_after, limit)?), Cw1155QueryMsg::AllTokens { start_after, limit } => { - to_binary(&query_all_tokens(deps, start_after, limit)?) + to_binary(&query::all_tokens(deps, start_after, limit)?) } } } - -pub fn query_balance(deps: Deps, owner: String, token_id: String) -> StdResult { - let owner = deps.api.addr_validate(&owner)?; - - let balance = BALANCES - .may_load(deps.storage, (&owner, &token_id))? - .unwrap_or_default(); - - Ok(BalanceResponse { balance }) -} - -pub fn query_batch_balance( - deps: Deps, - owner: String, - token_ids: Vec, -) -> StdResult { - let owner = deps.api.addr_validate(&owner)?; - - let balances = token_ids - .into_iter() - .map(|token_id| -> StdResult<_> { - Ok(BALANCES - .may_load(deps.storage, (&owner, &token_id))? - .unwrap_or_default()) - }) - .collect::>()?; - - Ok(BatchBalanceResponse { balances }) -} - -fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { - item.map(|(addr, expires)| cw1155::Approval { - spender: addr.into(), - expires, - }) -} - -pub fn query_approved_for_all( - deps: Deps, - env: Env, - owner: String, - include_expired: bool, - start_after: Option, - limit: Option, -) -> StdResult { - let owner = deps.api.addr_validate(&owner)?; - let start_after = maybe_addr(deps.api, start_after)?; - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_ref().map(Bound::exclusive); - - let operators = APPROVES - .prefix(&owner) - .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(build_approval) - .collect::>()?; - - Ok(ApprovedForAllResponse { operators }) -} - -pub fn query_token_info(deps: Deps, token_id: String) -> StdResult { - let url = TOKENS.load(deps.storage, &token_id)?; - - Ok(TokenInfoResponse { url }) -} - -pub fn query_is_approved_for_all( - deps: Deps, - env: Env, - owner: String, - operator: String, -) -> StdResult { - let owner_addr = deps.api.addr_validate(&owner)?; - let operator_addr = deps.api.addr_validate(&operator)?; - - let approved = check_can_approve(deps, &env, &owner_addr, &operator_addr)?; - - Ok(IsApprovedForAllResponse { approved }) -} - -pub fn query_tokens( - deps: Deps, - owner: String, - start_after: Option, - limit: Option, -) -> StdResult { - let owner = deps.api.addr_validate(&owner)?; - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); - - let tokens = BALANCES - .prefix(&owner) - .keys(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>()?; - - Ok(TokensResponse { tokens }) -} - -pub fn query_all_tokens( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); - - let tokens = TOKENS - .keys(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>()?; - - Ok(TokensResponse { tokens }) -} diff --git a/contracts/cw1155-base/src/execute.rs b/contracts/cw1155-base/src/execute.rs new file mode 100644 index 000000000..ab5a56d78 --- /dev/null +++ b/contracts/cw1155-base/src/execute.rs @@ -0,0 +1,312 @@ +use cosmwasm_std::{Addr, Binary, DepsMut, Response, StdResult, SubMsg, Uint128}; +use cw1155::{ApproveAllEvent, Cw1155BatchReceiveMsg, Cw1155ReceiveMsg, TokenId, TransferEvent}; +use cw_utils::{Event, Expiration}; + +use crate::{ + contract::ExecuteEnv, + helpers::guard_can_approve, + state::{APPROVES, BALANCES, MINTER, TOKENS}, + ContractError, +}; + +/// When from is None: mint new coins +/// When to is None: burn coins +/// When both are None: no token balance is changed, pointless but valid +/// +/// Make sure permissions are checked before calling this. +fn transfer_inner<'a>( + deps: &'a mut DepsMut, + from: Option<&'a Addr>, + to: Option<&'a Addr>, + token_id: &'a str, + amount: Uint128, +) -> Result, ContractError> { + if let Some(from_addr) = from { + BALANCES.update( + deps.storage, + (from_addr, token_id), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_sub(amount)?) + }, + )?; + } + + if let Some(to_addr) = to { + BALANCES.update( + deps.storage, + (to_addr, token_id), + |balance: Option| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_add(amount)?) + }, + )?; + } + + Ok(TransferEvent { + from: from.map(|x| x.as_ref()), + to: to.map(|x| x.as_ref()), + token_id, + amount, + }) +} + +pub fn send_from( + env: ExecuteEnv, + from: String, + to: String, + token_id: TokenId, + amount: Uint128, + msg: Option, +) -> Result { + let from_addr = env.deps.api.addr_validate(&from)?; + let to_addr = env.deps.api.addr_validate(&to)?; + + let ExecuteEnv { + mut deps, + env, + info, + } = env; + + guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; + + let mut rsp = Response::default(); + + let event = transfer_inner( + &mut deps, + Some(&from_addr), + Some(&to_addr), + &token_id, + amount, + )?; + event.add_attributes(&mut rsp); + + if let Some(msg) = msg { + rsp.messages = vec![SubMsg::new( + Cw1155ReceiveMsg { + operator: info.sender.to_string(), + from: Some(from), + amount, + token_id: token_id.clone(), + msg, + } + .into_cosmos_msg(to)?, + )] + } + + Ok(rsp) +} + +pub fn mint( + env: ExecuteEnv, + to: String, + token_id: TokenId, + amount: Uint128, + msg: Option, +) -> Result { + let ExecuteEnv { mut deps, info, .. } = env; + + let to_addr = deps.api.addr_validate(&to)?; + + if info.sender != MINTER.load(deps.storage)? { + return Err(ContractError::Unauthorized {}); + } + + let mut rsp = Response::default(); + + let event = transfer_inner(&mut deps, None, Some(&to_addr), &token_id, amount)?; + event.add_attributes(&mut rsp); + + if let Some(msg) = msg { + rsp.messages = vec![SubMsg::new( + Cw1155ReceiveMsg { + operator: info.sender.to_string(), + from: None, + amount, + token_id: token_id.clone(), + msg, + } + .into_cosmos_msg(to)?, + )] + } + + // insert if not exist + if !TOKENS.has(deps.storage, &token_id) { + // we must save some valid data here + TOKENS.save(deps.storage, &token_id, &String::new())?; + } + + Ok(rsp) +} + +pub fn burn( + env: ExecuteEnv, + from: String, + token_id: TokenId, + amount: Uint128, +) -> Result { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + + let from_addr = deps.api.addr_validate(&from)?; + + // whoever can transfer these tokens can burn + guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; + + let mut rsp = Response::default(); + let event = transfer_inner(&mut deps, Some(&from_addr), None, &token_id, amount)?; + event.add_attributes(&mut rsp); + Ok(rsp) +} + +pub fn batch_send_from( + env: ExecuteEnv, + from: String, + to: String, + batch: Vec<(TokenId, Uint128)>, + msg: Option, +) -> Result { + let ExecuteEnv { + mut deps, + env, + info, + } = env; + + let from_addr = deps.api.addr_validate(&from)?; + let to_addr = deps.api.addr_validate(&to)?; + + guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; + + let mut rsp = Response::default(); + for (token_id, amount) in batch.iter() { + let event = transfer_inner( + &mut deps, + Some(&from_addr), + Some(&to_addr), + token_id, + *amount, + )?; + event.add_attributes(&mut rsp); + } + + if let Some(msg) = msg { + rsp.messages = vec![SubMsg::new( + Cw1155BatchReceiveMsg { + operator: info.sender.to_string(), + from: Some(from), + batch, + msg, + } + .into_cosmos_msg(to)?, + )] + }; + + Ok(rsp) +} + +pub fn batch_mint( + env: ExecuteEnv, + to: String, + batch: Vec<(TokenId, Uint128)>, + msg: Option, +) -> Result { + let ExecuteEnv { mut deps, info, .. } = env; + if info.sender != MINTER.load(deps.storage)? { + return Err(ContractError::Unauthorized {}); + } + + let to_addr = deps.api.addr_validate(&to)?; + + let mut rsp = Response::default(); + + for (token_id, amount) in batch.iter() { + let event = transfer_inner(&mut deps, None, Some(&to_addr), token_id, *amount)?; + event.add_attributes(&mut rsp); + + // insert if not exist + if !TOKENS.has(deps.storage, token_id) { + // we must save some valid data here + TOKENS.save(deps.storage, token_id, &String::new())?; + } + } + + if let Some(msg) = msg { + rsp.messages = vec![SubMsg::new( + Cw1155BatchReceiveMsg { + operator: info.sender.to_string(), + from: None, + batch, + msg, + } + .into_cosmos_msg(to)?, + )] + }; + + Ok(rsp) +} + +pub fn batch_burn( + env: ExecuteEnv, + from: String, + batch: Vec<(TokenId, Uint128)>, +) -> Result { + let ExecuteEnv { + mut deps, + info, + env, + } = env; + + let from_addr = deps.api.addr_validate(&from)?; + + guard_can_approve(deps.as_ref(), &env, &from_addr, &info.sender)?; + + let mut rsp = Response::default(); + for (token_id, amount) in batch.into_iter() { + let event = transfer_inner(&mut deps, Some(&from_addr), None, &token_id, amount)?; + event.add_attributes(&mut rsp); + } + Ok(rsp) +} + +pub fn approve_all( + env: ExecuteEnv, + operator: String, + expires: Option, +) -> Result { + let ExecuteEnv { deps, info, env } = env; + + // reject expired data as invalid + let expires = expires.unwrap_or_default(); + if expires.is_expired(&env.block) { + return Err(ContractError::Expired {}); + } + + // set the operator for us + let operator_addr = deps.api.addr_validate(&operator)?; + APPROVES.save(deps.storage, (&info.sender, &operator_addr), &expires)?; + + let mut rsp = Response::default(); + ApproveAllEvent { + sender: info.sender.as_ref(), + operator: &operator, + approved: true, + } + .add_attributes(&mut rsp); + Ok(rsp) +} + +pub fn revoke_all(env: ExecuteEnv, operator: String) -> Result { + let ExecuteEnv { deps, info, .. } = env; + let operator_addr = deps.api.addr_validate(&operator)?; + APPROVES.remove(deps.storage, (&info.sender, &operator_addr)); + + let mut rsp = Response::default(); + ApproveAllEvent { + sender: info.sender.as_ref(), + operator: &operator, + approved: false, + } + .add_attributes(&mut rsp); + Ok(rsp) +} diff --git a/contracts/cw1155-base/src/helpers.rs b/contracts/cw1155-base/src/helpers.rs new file mode 100644 index 000000000..638054f3e --- /dev/null +++ b/contracts/cw1155-base/src/helpers.rs @@ -0,0 +1,30 @@ +use cosmwasm_std::{Addr, Deps, Env, StdResult}; + +use crate::{state::APPROVES, ContractError}; + +/// returns true if the sender can execute approve or reject on the contract +pub fn check_can_approve(deps: Deps, env: &Env, owner: &Addr, operator: &Addr) -> StdResult { + // owner can approve + if owner == operator { + return Ok(true); + } + // operator can approve + let op = APPROVES.may_load(deps.storage, (owner, operator))?; + Ok(match op { + Some(ex) => !ex.is_expired(&env.block), + None => false, + }) +} + +pub fn guard_can_approve( + deps: Deps, + env: &Env, + owner: &Addr, + operator: &Addr, +) -> Result<(), ContractError> { + if !check_can_approve(deps, env, owner, operator)? { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } +} diff --git a/contracts/cw1155-base/src/lib.rs b/contracts/cw1155-base/src/lib.rs index 69b809fe1..0aa98a4f1 100644 --- a/contracts/cw1155-base/src/lib.rs +++ b/contracts/cw1155-base/src/lib.rs @@ -1,6 +1,9 @@ pub mod contract; mod error; +pub mod execute; +pub mod helpers; pub mod msg; +pub mod query; pub mod state; pub use crate::error::ContractError; diff --git a/contracts/cw1155-base/src/query.rs b/contracts/cw1155-base/src/query.rs new file mode 100644 index 000000000..0b89b0cba --- /dev/null +++ b/contracts/cw1155-base/src/query.rs @@ -0,0 +1,130 @@ +use cosmwasm_std::{Addr, Deps, Env, Order, StdResult}; +use cw1155::{ + ApprovedForAllResponse, BalanceResponse, BatchBalanceResponse, IsApprovedForAllResponse, + TokenInfoResponse, TokensResponse, +}; +use cw_storage_plus::Bound; +use cw_utils::{maybe_addr, Expiration}; + +use crate::{ + helpers::check_can_approve, + state::{APPROVES, BALANCES, TOKENS}, +}; + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 30; + +pub fn balance(deps: Deps, owner: String, token_id: String) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let balance = BALANCES + .may_load(deps.storage, (&owner, &token_id))? + .unwrap_or_default(); + + Ok(BalanceResponse { balance }) +} + +pub fn batch_balance( + deps: Deps, + owner: String, + token_ids: Vec, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let balances = token_ids + .into_iter() + .map(|token_id| -> StdResult<_> { + Ok(BALANCES + .may_load(deps.storage, (&owner, &token_id))? + .unwrap_or_default()) + }) + .collect::>()?; + + Ok(BatchBalanceResponse { balances }) +} + +fn build_approval(item: StdResult<(Addr, Expiration)>) -> StdResult { + item.map(|(addr, expires)| cw1155::Approval { + spender: addr.into(), + expires, + }) +} + +pub fn approved_for_all( + deps: Deps, + env: Env, + owner: String, + include_expired: bool, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + let start_after = maybe_addr(deps.api, start_after)?; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(Bound::exclusive); + + let operators = APPROVES + .prefix(&owner) + .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(build_approval) + .collect::>()?; + + Ok(ApprovedForAllResponse { operators }) +} + +pub fn token_info(deps: Deps, token_id: String) -> StdResult { + let url = TOKENS.load(deps.storage, &token_id)?; + + Ok(TokenInfoResponse { url }) +} + +pub fn is_approved_for_all( + deps: Deps, + env: Env, + owner: String, + operator: String, +) -> StdResult { + let owner_addr = deps.api.addr_validate(&owner)?; + let operator_addr = deps.api.addr_validate(&operator)?; + + let approved = check_can_approve(deps, &env, &owner_addr, &operator_addr)?; + + Ok(IsApprovedForAllResponse { approved }) +} + +pub fn tokens( + deps: Deps, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + + let tokens = BALANCES + .prefix(&owner) + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>()?; + + Ok(TokensResponse { tokens }) +} + +pub fn all_tokens( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_ref().map(|s| Bound::exclusive(s.as_str())); + + let tokens = TOKENS + .keys(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>()?; + + Ok(TokensResponse { tokens }) +}