From bdd806b06e27fad4d7ed4a7c107c0bb499ce6c46 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Sun, 14 May 2023 12:55:08 -0700 Subject: [PATCH] Reorganize code structure. --- contracts/cw721-piggy-bank/README.md | 6 +- contracts/cw721-piggy-bank/examples/schema.rs | 2 +- contracts/cw721-piggy-bank/src/contract.rs | 200 ++++++++ contracts/cw721-piggy-bank/src/error.rs | 20 + contracts/cw721-piggy-bank/src/lib.rs | 457 +----------------- contracts/cw721-piggy-bank/src/msg.rs | 64 +++ contracts/cw721-piggy-bank/src/state.rs | 7 + contracts/cw721-piggy-bank/src/tests.rs | 173 +++++++ 8 files changed, 475 insertions(+), 454 deletions(-) create mode 100644 contracts/cw721-piggy-bank/src/contract.rs create mode 100644 contracts/cw721-piggy-bank/src/error.rs create mode 100644 contracts/cw721-piggy-bank/src/msg.rs create mode 100644 contracts/cw721-piggy-bank/src/state.rs create mode 100644 contracts/cw721-piggy-bank/src/tests.rs diff --git a/contracts/cw721-piggy-bank/README.md b/contracts/cw721-piggy-bank/README.md index 268c0c9..0677504 100644 --- a/contracts/cw721-piggy-bank/README.md +++ b/contracts/cw721-piggy-bank/README.md @@ -24,7 +24,7 @@ There are a couple of approaches we can use to create dynamic NFTs: - Frontend code that handles the image generation based on on-chain metadata (may not always display correctly in wallets) - Update token URI in the contract based on on-chain events -There are tradeoffs for all of these, but we are going to go for the last approach. +There are tradeoffs for all of these, but we are going to go for the last approach. **Dynamic NFT Example: Trees** @@ -34,7 +34,7 @@ Let's use the example of a tree that grows the more we feed it with carbon credi Here's a potential folder structure we could use: -``` +```ignore ./metadata /1 seedling.json @@ -54,4 +54,4 @@ As more funds are deposited in the NFT, we would have logic to update it's `toke For example, when one token has been deposited, we update the `token_uri` to `//sapling.json`, when ten tokens have been deposited we update it to `//tree.json`, and when one hundred tokens have been deposited we update it to `//fullgrown.json`. -One thing to note is that the folder structure of the metadata must be in-sync with the logic of the contract. \ No newline at end of file +One thing to note is that the folder structure of the metadata must be in-sync with the logic of the contract. diff --git a/contracts/cw721-piggy-bank/examples/schema.rs b/contracts/cw721-piggy-bank/examples/schema.rs index 95e98a4..dd56f75 100644 --- a/contracts/cw721-piggy-bank/examples/schema.rs +++ b/contracts/cw721-piggy-bank/examples/schema.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::write_api; -use cw721_piggy_bank::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cw721_piggy_bank::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { write_api! { diff --git a/contracts/cw721-piggy-bank/src/contract.rs b/contracts/cw721-piggy-bank/src/contract.rs new file mode 100644 index 0000000..dd11106 --- /dev/null +++ b/contracts/cw721-piggy-bank/src/contract.rs @@ -0,0 +1,200 @@ +use cosmwasm_std::{ + entry_point, to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo, Response, + StdResult, Uint128, +}; +pub use cw721_base::{ + ContractError as BaseContractError, InstantiateMsg as BaseInstantiateMsg, MinterResponse, +}; +use cw_utils::must_pay; + +use crate::{ + msg::{Cw721Contract, ExecuteExt, ExecuteMsg, InstantiateMsg, QueryExt, QueryMsg}, + state::{BALANCES, DENOM}, + ContractError, +}; + +// Version info for migration +pub const CONTRACT_NAME: &str = "crates.io:cw721-piggy-bank"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// This makes a conscious choice on the various generics used by the contract +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + // TODO Validate denoms are formated correctly + + // Save denoms + DENOM.save(deps.storage, &msg.deposit_denom)?; + + // Instantiate the base contract + Cw721Contract::default().instantiate( + deps.branch(), + env, + info, + BaseInstantiateMsg { + minter: msg.minter, + name: msg.name, + symbol: msg.symbol, + }, + ) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + // Optionally override the default cw721-base behavior + ExecuteMsg::Burn { token_id } => execute_burn(deps, env, info, token_id), + + // Implment extension messages here, remove if you don't wish to use + // An ExecuteExt extension + ExecuteMsg::Extension { msg } => match msg { + ExecuteExt::Deposit { token_id } => execute_deposit(deps, env, info, token_id), + ExecuteExt::UpdateTokenUri { + token_id, + token_uri, + } => execute_update_token_uri(deps, env, info, token_id, token_uri), + }, + + // Use the default cw721-base implementation + _ => Cw721Contract::default() + .execute(deps, env, info, msg) + .map_err(Into::into), + } +} + +pub fn execute_update_token_uri( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + token_uri: String, +) -> Result { + let base = Cw721Contract::default(); + + // Check minter / admin to update token_uri + let minter = base.minter(deps.as_ref())?; + match minter.minter { + Some(minter) => { + if info.sender != minter { + return Err(ContractError::Unauthorized {}); + } + } + None => { + return Err(ContractError::Unauthorized {}); + } + } + + // Update token_uri + let mut token = base.tokens.load(deps.storage, &token_id)?; + token.token_uri = Some(token_uri.clone()); + base.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_attribute("action", "update_token_uri") + .add_attribute("token_id", token_id) + .add_attribute("token_uri", token_uri)) +} + +pub fn execute_burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, +) -> Result { + let base = Cw721Contract::default(); + + let denom = DENOM.load(deps.storage)?; + + // Pay out the piggy bank! + let balance = BALANCES.may_load(deps.storage, &token_id)?; + let msgs = match balance { + Some(balance) => { + vec![BankMsg::Send { + to_address: info.sender.to_string(), + amount: vec![Coin { + denom, + amount: balance, + }], + }] + } + None => vec![], + }; + + // Pass off to default cw721 burn implementation, handles checking ownership + base.execute(deps, env, info, ExecuteMsg::Burn { token_id })?; + + Ok(Response::default().add_messages(msgs)) +} + +pub fn execute_deposit( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, +) -> Result { + // Check that funds were actually sent + let denom = DENOM.load(deps.storage)?; + // Check the right kind of funds were sent + let amount = must_pay(&info, &denom)?; + + let base = Cw721Contract::default(); + + // Check that the token exists + let mut token = base.tokens.load(deps.storage, &token_id)?; + + BALANCES.update(deps.storage, &token_id, |balance| -> StdResult<_> { + let new_balance = balance.unwrap_or_default() + amount; + + // TODO don't hard code + let base_url = ""; + + // Native token micro units are typically 6 decimal places + // Check if balance is greater than 1 + if new_balance > Uint128::new(1000000) { + token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "sapling.json")); + } else if new_balance > Uint128::new(10000000) { + token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "tree.json")); + } else { + token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "fullgrown.json")); + } + + Ok(new_balance) + })?; + + base.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_attribute("action", "deposit") + .add_attribute("value", amount.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + // Optionally override a default cw721-base query + // QueryMsg::Minter {} => unimplemented!(), + QueryMsg::Extension { msg } => match msg { + // Returns Coin type for the ballance of an NFT + QueryExt::Balance { token_id } => to_binary(&Coin { + denom: DENOM.load(deps.storage)?, + amount: BALANCES + .may_load(deps.storage, &token_id)? + .unwrap_or_default(), + }), + }, + + // Use default cw721-base query implementation + _ => Cw721Contract::default().query(deps, env, msg), + } +} diff --git a/contracts/cw721-piggy-bank/src/error.rs b/contracts/cw721-piggy-bank/src/error.rs new file mode 100644 index 0000000..b507bfc --- /dev/null +++ b/contracts/cw721-piggy-bank/src/error.rs @@ -0,0 +1,20 @@ +use cosmwasm_std::StdError; +use cw_utils::PaymentError; +use thiserror::Error; + +/// Custom errors for this contract +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("Unauthorized")] + Unauthorized {}, + + /// This inherits from cw721-base::ContractError to handle the base contract errors + #[error("NFT contract error: {0}")] + Cw721Error(#[from] cw721_base::ContractError), +} \ No newline at end of file diff --git a/contracts/cw721-piggy-bank/src/lib.rs b/contracts/cw721-piggy-bank/src/lib.rs index 6c03fa8..d1800ad 100644 --- a/contracts/cw721-piggy-bank/src/lib.rs +++ b/contracts/cw721-piggy-bank/src/lib.rs @@ -1,454 +1,11 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Coin, CustomMsg, Empty, StdError, Uint128}; -pub use cw721_base::{ - ContractError as BaseContractError, InstantiateMsg as BaseInstantiateMsg, MinterResponse, -}; -use cw_storage_plus::{Item, Map}; -use cw_utils::PaymentError; -use thiserror::Error; +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] -// Version info for migration -const CONTRACT_NAME: &str = "crates.io:cw721-piggy-bank"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -// Implements extended on-chain metadata, by default cw721 NFTs only store a -// token_uri, which is a URL to off-chain metadata (same as ERC721). -#[cw_serde] -#[derive(Default)] -pub struct MetadataExt { - // TODO support showing different token_uris based on how much is deposited -} - -// This is the custom Execute message extension for this contract. -// Use it to implement custom functionality. -#[cw_serde] -pub enum ExecuteExt { - /// Used to deposit funds in a particular NFT - Deposit { token_id: String }, - UpdateTokenUri { - token_id: String, - token_uri: String, - }, -} -impl CustomMsg for ExecuteExt {} - -// This is the custom Query message type for this contract. -// Use it to implement custom query messages. -#[cw_serde] -pub enum QueryExt { - /// Query the current balance for an individual NFT - Balance { token_id: String }, -} -impl CustomMsg for QueryExt {} - -// This contrains default cw721 logic with extensions. -// If you don't need a particular extension, replace it with an -// `Empty` type. -pub type Cw721Contract<'a> = - cw721_base::Cw721Contract<'a, MetadataExt, Empty, ExecuteExt, QueryExt>; - -#[cw_serde] -pub struct InstantiateMsg { - /// Name of the NFT contract - pub name: String, - /// Symbol of the NFT contract - pub symbol: String, - - /// The minter is the only one who can create new NFTs. - /// This is designed for a base NFT that is controlled by an external program - /// or contract. You will likely replace this with custom logic in custom NFTs - pub minter: String, - - /// Allowed denoms for deposit - pub deposit_denom: String, -} - -// The execute message type for this contract. -// If you don't need the Metadata and Execute extensions, you can use the -// `Empty` type. -pub type ExecuteMsg = cw721_base::ExecuteMsg; - -// The query message type for this contract. -// If you don't need the QueryExt extension, you can use the -// `Empty` type. -pub type QueryMsg = cw721_base::QueryMsg; - -/// Custom errors for this contract -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("{0}")] - PaymentError(#[from] PaymentError), - - #[error("Unauthorized")] - Unauthorized {}, - - /// This inherits from cw721-base::ContractError to handle the base contract errors - #[error("NFT contract error: {0}")] - Cw721Error(#[from] cw721_base::ContractError), -} - -/// Map for storing NFT balances (token_id, amount) -pub const BALANCES: Map<&str, Uint128> = Map::new("nft_balances"); - -pub const DENOM: Item = Item::new("denoms"); - -#[cfg(not(feature = "library"))] -pub mod entry { - use super::*; - - use cosmwasm_std::{entry_point, to_binary, BankMsg}; - use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; - use cw_utils::must_pay; - - // This makes a conscious choice on the various generics used by the contract - #[entry_point] - pub fn instantiate( - mut deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, - ) -> StdResult { - cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // TODO Validate denoms are formated correctly - - // Save denoms - DENOM.save(deps.storage, &msg.deposit_denom)?; - - // Instantiate the base contract - Cw721Contract::default().instantiate( - deps.branch(), - env, - info, - BaseInstantiateMsg { - minter: msg.minter, - name: msg.name, - symbol: msg.symbol, - }, - ) - } - - #[entry_point] - pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, - ) -> Result { - match msg { - // Optionally override the default cw721-base behavior - ExecuteMsg::Burn { token_id } => execute_burn(deps, env, info, token_id), - - // Implment extension messages here, remove if you don't wish to use - // An ExecuteExt extension - ExecuteMsg::Extension { msg } => match msg { - ExecuteExt::Deposit { token_id } => execute_deposit(deps, env, info, token_id), - ExecuteExt::UpdateTokenUri { token_id, token_uri } => execute_update_token_uri(deps, env, info, token_id, token_uri), - }, - - // Use the default cw721-base implementation - _ => Cw721Contract::default() - .execute(deps, env, info, msg) - .map_err(Into::into), - } - } - - pub fn execute_update_token_uri( - deps: DepsMut, - env: Env, - info: MessageInfo, - token_id: String, - token_uri: String, - ) -> Result { - let base = Cw721Contract::default(); - - // Check minter / admin to update token_uri - let minter = base.minter(deps.as_ref())?; - match minter.minter { - Some(minter) => { - if info.sender != minter { - return Err(ContractError::Unauthorized {}); - } - }, - None => { - return Err(ContractError::Unauthorized {}); - } - } - - // Update token_uri - let mut token = base.tokens.load(deps.storage, &token_id)?; - token.token_uri = Some(token_uri.clone()); - base.tokens.save(deps.storage, &token_id, &token)?; - - Ok(Response::default().add_attribute("action", "update_token_uri").add_attribute("token_id", token_id).add_attribute("token_uri", token_uri)) - } - - pub fn execute_burn( - deps: DepsMut, - env: Env, - info: MessageInfo, - token_id: String, - ) -> Result { - let base = Cw721Contract::default(); - - let denom = DENOM.load(deps.storage)?; - - // Pay out the piggy bank! - let balance = BALANCES.may_load(deps.storage, &token_id)?; - let msgs = match balance { - Some(balance) => { - vec![BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin { - denom, - amount: balance, - }], - }] - } - None => vec![], - }; - - // Pass off to default cw721 burn implementation, handles checking ownership - base.execute(deps, env, info, ExecuteMsg::Burn { token_id })?; - - Ok(Response::default().add_messages(msgs)) - } - - pub fn execute_deposit( - deps: DepsMut, - _env: Env, - info: MessageInfo, - token_id: String, - ) -> Result { - // Check that funds were actually sent - let denom = DENOM.load(deps.storage)?; - // Check the right kind of funds were sent - let amount = must_pay(&info, &denom)?; - - let base = Cw721Contract::default(); - - // Check that the token exists - let mut token = base.tokens.load(deps.storage, &token_id)?; - - BALANCES.update(deps.storage, &token_id, |balance| -> StdResult<_> { - let new_balance = balance.unwrap_or_default() + amount; - - // TODO don't hard code - let base_url = ""; - - // Native token micro units are typically 6 decimal places - // Check if balance is greater than 1 - if new_balance > Uint128::new(1000000) { - token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "sapling.json")); - } else if new_balance > Uint128::new(10000000) { - token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "tree.json")); - } else { - token.token_uri = Some(format!("{}/{}/{}", base_url, token_id, "fullgrown.json")); - } - - Ok(new_balance) - })?; - - base.tokens.save(deps.storage, &token_id, &token)?; - - Ok(Response::default() - .add_attribute("action", "deposit") - .add_attribute("value", amount.to_string())) - } - - #[entry_point] - pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - match msg { - // Optionally override a default cw721-base query - // QueryMsg::Minter {} => unimplemented!(), - QueryMsg::Extension { msg } => match msg { - // Returns Coin type for the ballance of an NFT - QueryExt::Balance { token_id } => to_binary(&Coin { - denom: DENOM.load(deps.storage)?, - amount: BALANCES - .may_load(deps.storage, &token_id)? - .unwrap_or_default(), - }), - }, - - // Use default cw721-base query implementation - _ => Cw721Contract::default().query(deps, env, msg), - } - } -} +pub mod contract; +mod error; +pub mod msg; +pub mod state; #[cfg(test)] -mod tests { - use super::*; - - use cosmwasm_std::{ - coins, - testing::{mock_dependencies, mock_env, mock_info}, - BankMsg, CosmosMsg, - }; - - /// Make sure cw2 version info is properly initialized during instantiation, - /// and NOT overwritten by the base contract. - #[test] - fn proper_cw2_initialization() { - let mut deps = mock_dependencies(); - - entry::instantiate( - deps.as_mut(), - mock_env(), - mock_info("larry", &[]), - InstantiateMsg { - name: "".into(), - symbol: "".into(), - minter: "larry".into(), - deposit_denom: "ujuno".into(), - }, - ) - .unwrap(); - - let version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); - assert_eq!(version.contract, CONTRACT_NAME); - assert_ne!(version.contract, cw721_base::CONTRACT_NAME); - } - - #[test] - fn happy_path() { - let mut deps = mock_dependencies(); - const BOB: &str = "bob"; - - entry::instantiate( - deps.as_mut(), - mock_env(), - mock_info(BOB, &[]), - InstantiateMsg { - name: "1337".into(), - symbol: "1337".into(), - minter: BOB.into(), - deposit_denom: "ujuno".into(), - }, - ) - .unwrap(); - - // Mint the NFT - entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &[]), - ExecuteMsg::Mint { - token_id: "1".into(), - owner: BOB.into(), - token_uri: Some("https://ipfs.io/cutedog.json".to_string()), - extension: MetadataExt {}, - }, - ) - .unwrap(); - - // Calling deposit funds without funds errors - entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &[]), - ExecuteMsg::Extension { - msg: ExecuteExt::Deposit { - token_id: "1".to_string(), - }, - }, - ) - .unwrap_err(); - - // Calling deposit with wrong denom errors - entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &coins(1000, "uatom")), - ExecuteMsg::Extension { - msg: ExecuteExt::Deposit { - token_id: "1".to_string(), - }, - }, - ) - .unwrap_err(); - - // Can't deposit to token id that doesn't exist - let err = entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &coins(1000, "ujuno")), - ExecuteMsg::Extension { - msg: ExecuteExt::Deposit { - token_id: "3".to_string(), - }, - }, - ) - .unwrap_err(); - assert_eq!( - err, - ContractError::Std(StdError::NotFound { - kind: "cw721_base::state::TokenInfo".to_string() - }) - ); - - // Calling deposit succeeds with correct token - entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &coins(1000, "ujuno")), - ExecuteMsg::Extension { - msg: ExecuteExt::Deposit { - token_id: "1".to_string(), - }, - }, - ) - .unwrap(); - - // Only owner can burn NFT - entry::execute( - deps.as_mut(), - mock_env(), - mock_info("rando", &[]), - ExecuteMsg::Burn { - token_id: "1".to_string(), - }, - ) - .unwrap_err(); - - // Can't burn an NFT that doesn't exist - let err = entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &[]), - ExecuteMsg::Burn { - token_id: "2".to_string(), - }, - ) - .unwrap_err(); - assert_eq!( - err, - ContractError::Cw721Error(cw721_base::ContractError::Std(StdError::NotFound { - kind: "cw721_base::state::TokenInfo".to_string() - })) - ); - - // Test burning NFT returns money - let res = entry::execute( - deps.as_mut(), - mock_env(), - mock_info(BOB, &[]), - ExecuteMsg::Burn { - token_id: "1".to_string(), - }, - ) - .unwrap(); +mod tests; - assert_eq!( - res.messages[0].msg, - CosmosMsg::Bank(BankMsg::Send { - to_address: BOB.to_string(), - amount: coins(1000, "ujuno"), - }) - ); - } -} +pub use crate::error::ContractError; diff --git a/contracts/cw721-piggy-bank/src/msg.rs b/contracts/cw721-piggy-bank/src/msg.rs new file mode 100644 index 0000000..9e99294 --- /dev/null +++ b/contracts/cw721-piggy-bank/src/msg.rs @@ -0,0 +1,64 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{CustomMsg, Empty}; + +// Implements extended on-chain metadata, by default cw721 NFTs only store a +// token_uri, which is a URL to off-chain metadata (same as ERC721). +#[cw_serde] +#[derive(Default)] +pub struct MetadataExt { + // TODO support showing different token_uris based on how much is deposited +} + +// This is the custom Execute message extension for this contract. +// Use it to implement custom functionality. +#[cw_serde] +pub enum ExecuteExt { + /// Used to deposit funds in a particular NFT + Deposit { token_id: String }, + UpdateTokenUri { + token_id: String, + token_uri: String, + }, +} +impl CustomMsg for ExecuteExt {} + +// This is the custom Query message type for this contract. +// Use it to implement custom query messages. +#[cw_serde] +pub enum QueryExt { + /// Query the current balance for an individual NFT + Balance { token_id: String }, +} +impl CustomMsg for QueryExt {} + +// This contrains default cw721 logic with extensions. +// If you don't need a particular extension, replace it with an +// `Empty` type. +pub type Cw721Contract<'a> = + cw721_base::Cw721Contract<'a, MetadataExt, Empty, ExecuteExt, QueryExt>; + +#[cw_serde] +pub struct InstantiateMsg { + /// Name of the NFT contract + pub name: String, + /// Symbol of the NFT contract + pub symbol: String, + + /// The minter is the only one who can create new NFTs. + /// This is designed for a base NFT that is controlled by an external program + /// or contract. You will likely replace this with custom logic in custom NFTs + pub minter: String, + + /// Allowed denoms for deposit + pub deposit_denom: String, +} + +// The execute message type for this contract. +// If you don't need the Metadata and Execute extensions, you can use the +// `Empty` type. +pub type ExecuteMsg = cw721_base::ExecuteMsg; + +// The query message type for this contract. +// If you don't need the QueryExt extension, you can use the +// `Empty` type. +pub type QueryMsg = cw721_base::QueryMsg; diff --git a/contracts/cw721-piggy-bank/src/state.rs b/contracts/cw721-piggy-bank/src/state.rs new file mode 100644 index 0000000..697e914 --- /dev/null +++ b/contracts/cw721-piggy-bank/src/state.rs @@ -0,0 +1,7 @@ +use cosmwasm_std::Uint128; +use cw_storage_plus::{Map, Item}; + +/// Map for storing NFT balances (token_id, amount) +pub const BALANCES: Map<&str, Uint128> = Map::new("nft_balances"); + +pub const DENOM: Item = Item::new("denoms"); \ No newline at end of file diff --git a/contracts/cw721-piggy-bank/src/tests.rs b/contracts/cw721-piggy-bank/src/tests.rs new file mode 100644 index 0000000..e8432a9 --- /dev/null +++ b/contracts/cw721-piggy-bank/src/tests.rs @@ -0,0 +1,173 @@ +use crate::{ + contract::{execute, instantiate, CONTRACT_NAME}, + msg::{ExecuteExt, ExecuteMsg, InstantiateMsg, MetadataExt}, + ContractError, +}; + +use cosmwasm_std::{ + coins, + testing::{mock_dependencies, mock_env, mock_info}, + BankMsg, CosmosMsg, StdError, +}; + +/// Make sure cw2 version info is properly initialized during instantiation, +/// and NOT overwritten by the base contract. +#[test] +fn proper_cw2_initialization() { + let mut deps = mock_dependencies(); + + instantiate( + deps.as_mut(), + mock_env(), + mock_info("larry", &[]), + InstantiateMsg { + name: "".into(), + symbol: "".into(), + minter: "larry".into(), + deposit_denom: "ujuno".into(), + }, + ) + .unwrap(); + + let version = cw2::get_contract_version(deps.as_ref().storage).unwrap(); + assert_eq!(version.contract, CONTRACT_NAME); + assert_ne!(version.contract, cw721_base::CONTRACT_NAME); +} + +#[test] +fn happy_path() { + let mut deps = mock_dependencies(); + const BOB: &str = "bob"; + + instantiate( + deps.as_mut(), + mock_env(), + mock_info(BOB, &[]), + InstantiateMsg { + name: "1337".into(), + symbol: "1337".into(), + minter: BOB.into(), + deposit_denom: "ujuno".into(), + }, + ) + .unwrap(); + + // Mint the NFT + execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &[]), + ExecuteMsg::Mint { + token_id: "1".into(), + owner: BOB.into(), + token_uri: Some("https://ipfs.io/cutedog.json".to_string()), + extension: MetadataExt {}, + }, + ) + .unwrap(); + + // Calling deposit funds without funds errors + execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &[]), + ExecuteMsg::Extension { + msg: ExecuteExt::Deposit { + token_id: "1".to_string(), + }, + }, + ) + .unwrap_err(); + + // Calling deposit with wrong denom errors + execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &coins(1000, "uatom")), + ExecuteMsg::Extension { + msg: ExecuteExt::Deposit { + token_id: "1".to_string(), + }, + }, + ) + .unwrap_err(); + + // Can't deposit to token id that doesn't exist + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &coins(1000, "ujuno")), + ExecuteMsg::Extension { + msg: ExecuteExt::Deposit { + token_id: "3".to_string(), + }, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::NotFound { + kind: "cw721_base::state::TokenInfo".to_string() + }) + ); + + // Calling deposit succeeds with correct token + execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &coins(1000, "ujuno")), + ExecuteMsg::Extension { + msg: ExecuteExt::Deposit { + token_id: "1".to_string(), + }, + }, + ) + .unwrap(); + + // Only owner can burn NFT + execute( + deps.as_mut(), + mock_env(), + mock_info("rando", &[]), + ExecuteMsg::Burn { + token_id: "1".to_string(), + }, + ) + .unwrap_err(); + + // Can't burn an NFT that doesn't exist + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &[]), + ExecuteMsg::Burn { + token_id: "2".to_string(), + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::Cw721Error(cw721_base::ContractError::Std(StdError::NotFound { + kind: "cw721_base::state::TokenInfo".to_string() + })) + ); + + // Test burning NFT returns money + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(BOB, &[]), + ExecuteMsg::Burn { + token_id: "1".to_string(), + }, + ) + .unwrap(); + + assert_eq!( + res.messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { + to_address: BOB.to_string(), + amount: coins(1000, "ujuno"), + }) + ); +}