diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5b5c1d5..c6f2fa73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added Validator Staking ADO [(#330)](https://github.com/andromedaprotocol/andromeda-core/pull/330) - Added Restake and Redelegate to Validator Staking [(#622)](https://github.com/andromedaprotocol/andromeda-core/pull/622) - Added andromeda-math and andromeda-account packages[(#672)](https://github.com/andromedaprotocol/andromeda-core/pull/672) +- Added PoW Cw721 ADO [(#697)](https://github.com/andromedaprotocol/andromeda-core/pull/697) ### Changed diff --git a/Cargo.lock b/Cargo.lock index b3e206e39..449a94ddc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -762,6 +762,26 @@ dependencies = [ "cw-utils 1.0.3", ] +[[package]] +name = "andromeda-pow-cw721" +version = "0.1.0-b.1" +dependencies = [ + "andromeda-app", + "andromeda-non-fungible-tokens", + "andromeda-std", + "andromeda-testing", + "cosmwasm-schema 1.5.8", + "cosmwasm-std 1.5.8", + "cw-multi-test", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw721 0.18.0", + "cw721-base 0.18.0", + "sha2 0.10.8", + "test-case", +] + [[package]] name = "andromeda-primitive" version = "2.0.4" diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/.cargo/config b/contracts/non-fungible-tokens/andromeda-pow-cw721/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/Cargo.toml b/contracts/non-fungible-tokens/andromeda-pow-cw721/Cargo.toml new file mode 100644 index 000000000..4bb299509 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "andromeda-pow-cw721" +version = "0.1.0-b.1" +authors = ["Mitar Djakovic "] +edition = "2021" +rust-version = "1.75.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +testing = ["cw-multi-test", "andromeda-testing"] + + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true } +sha2 = { version = "0.10.8" } +test-case = { workspace = true } +# hex = { version = "0.4.3" } + +andromeda-std = { workspace = true } +andromeda-non-fungible-tokens = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +cw-multi-test = { workspace = true, optional = true } +andromeda-testing = { workspace = true, optional = true } +cw-orch = { workspace = true } +# cw-orch-daemon = "0.24.2" + +[dev-dependencies] +andromeda-app = { workspace = true } diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/examples/schema.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/examples/schema.rs new file mode 100644 index 000000000..2c2e470fc --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/examples/schema.rs @@ -0,0 +1,10 @@ +use andromeda_non_fungible_tokens::pow_cw721::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg + } +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/contract.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/contract.rs new file mode 100644 index 000000000..e57c2d07b --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/contract.rs @@ -0,0 +1,107 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError}; + +use andromeda_non_fungible_tokens::pow_cw721::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ + ado_base::{ + permissioning::{LocalPermission, Permission}, + InstantiateMsg as BaseInstantiateMsg, MigrateMsg, + }, + ado_contract::ADOContract, + common::{context::ExecuteContext, encode_binary}, + error::ContractError, +}; + +use crate::execute::handle_execute; +use crate::query::{query_linked_cw721_address, query_pow_nft}; +use crate::state::LINKED_CW721_ADDRESS; + +const CONTRACT_NAME: &str = "crates.io:andromeda-pow-cw721"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const MINT_POW_NFT_ACTION: &str = "MINT_POW_NFT"; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let inst_resp = ADOContract::default().instantiate( + deps.storage, + env.clone(), + deps.api, + &deps.querier, + info, + BaseInstantiateMsg { + ado_type: CONTRACT_NAME.to_string(), + ado_version: CONTRACT_VERSION.to_string(), + kernel_address: msg.kernel_address, + owner: msg.owner, + }, + )?; + + LINKED_CW721_ADDRESS.save(deps.storage, &msg.linked_cw721_address)?; + + // Set mint PoW NFT action permission + if let Some(authorized_origin_minter_addresses) = msg.authorized_origin_minter_addresses { + if !authorized_origin_minter_addresses.is_empty() { + ADOContract::default().permission_action(MINT_POW_NFT_ACTION, deps.storage)?; + } + + for origin_minter_address in authorized_origin_minter_addresses { + let addr = origin_minter_address.get_raw_address(&deps.as_ref())?; + ADOContract::set_permission( + deps.storage, + MINT_POW_NFT_ACTION, + addr, + Permission::Local(LocalPermission::Whitelisted(None)), + )?; + } + } + + Ok(inst_resp) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let ctx = ExecuteContext::new(deps, info, env); + match msg { + ExecuteMsg::AMPReceive(pkt) => { + ADOContract::default().execute_amp_receive(ctx, pkt, handle_execute) + } + _ => handle_execute(ctx, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetPowNFT { token_id } => encode_binary(&query_pow_nft(deps, token_id)?), + QueryMsg::GetLinkedCw721Address {} => encode_binary(&query_linked_cw721_address(deps)?), + _ => ADOContract::default().query(deps, env, msg), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + if msg.result.is_err() { + return Err(ContractError::Std(StdError::generic_err( + msg.result.unwrap_err(), + ))); + } + + Ok(Response::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + ADOContract::default().migrate(deps, CONTRACT_NAME, CONTRACT_VERSION) +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/execute.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/execute.rs new file mode 100644 index 000000000..65aef5efc --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/execute.rs @@ -0,0 +1,187 @@ +use andromeda_non_fungible_tokens::{ + cw721::{ExecuteMsg as AndrCw721ExecuteMsg, TokenExtension}, + pow_cw721::{ExecuteMsg, PowNFTInfo}, +}; +use andromeda_std::{ + ado_contract::ADOContract, + amp::AndrAddr, + common::{actions::call_action, context::ExecuteContext, encode_binary}, + error::ContractError, +}; +use cosmwasm_std::{Binary, CosmosMsg, Event, Response, WasmMsg}; +use sha2::{Digest, Sha256}; + +use crate::contract::MINT_POW_NFT_ACTION; +use crate::state::{LINKED_CW721_ADDRESS, POW_NFT}; + +pub fn handle_execute(mut ctx: ExecuteContext, msg: ExecuteMsg) -> Result { + let action_response = call_action( + &mut ctx.deps, + &ctx.info, + &ctx.env, + &ctx.amp_ctx, + msg.as_ref(), + )?; + + let res = match msg { + ExecuteMsg::MintPowNFT { + owner, + token_id, + token_uri, + extension, + base_difficulty, + } => execute_mint_pow_nft(ctx, owner, token_id, token_uri, extension, base_difficulty), + ExecuteMsg::SubmitProof { token_id, solution } => { + execute_submit_proof(ctx, token_id, solution) + } + _ => ADOContract::default().execute(ctx, msg), + }?; + + Ok(res + .add_submessages(action_response.messages) + .add_attributes(action_response.attributes) + .add_events(action_response.events)) +} + +fn execute_mint_pow_nft( + mut ctx: ExecuteContext, + owner: AndrAddr, + token_id: String, + token_uri: Option, + extension: TokenExtension, + base_difficulty: u64, +) -> Result { + if base_difficulty == 0 || base_difficulty > 128 { + return Err(ContractError::CustomError { + msg: "Base difficulty must be between 1 and 128".to_string(), + }); + } + + let sender = ctx.info.sender; + + ADOContract::default().is_permissioned( + ctx.deps.branch(), + ctx.env.clone(), + MINT_POW_NFT_ACTION, + sender.clone(), + )?; + + if POW_NFT + .may_load(ctx.deps.storage, token_id.clone())? + .is_some() + { + return Err(ContractError::CustomError { + msg: format!("Token ID {} already exists", token_id), + }); + } + + let owner_addr = owner.get_raw_address(&ctx.deps.as_ref())?; + let cw721_address = LINKED_CW721_ADDRESS.load(ctx.deps.storage)?; + + let addr = cw721_address + .get_raw_address(&ctx.deps.as_ref())? + .to_string(); + let mint_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: addr, + msg: encode_binary(&AndrCw721ExecuteMsg::Mint { + token_id: token_id.clone(), + owner: owner_addr.to_string(), + token_uri, + extension, + })?, + funds: vec![], + }); + + let block_height = ctx.env.block.height; + + let mut hasher = Sha256::new(); + hasher.update(block_height.to_be_bytes()); + let last_hash = hasher.finalize().to_vec(); + + let pow_nft_info = PowNFTInfo { + owner: owner_addr, + level: 1_u64, + last_hash: Binary(last_hash), + difficulty: base_difficulty, + }; + + POW_NFT.save(ctx.deps.storage, token_id, &pow_nft_info)?; + + Ok(Response::new() + .add_message(mint_msg) + .add_attribute("method", "mint_pow_nft") + .add_attribute("sender", sender)) +} + +fn execute_submit_proof( + ctx: ExecuteContext, + token_id: String, + solution: u128, +) -> Result { + let sender = ctx.info.sender; + let mut pow_nft = POW_NFT + .load(ctx.deps.storage, token_id.clone()) + .map_err(|_| ContractError::NFTNotFound {})?; + + let mut hasher = Sha256::new(); + hasher.update(&pow_nft.last_hash); + hasher.update(&solution.to_be_bytes()); + let hash = hasher.finalize(); + + let hash_value = u128::from_be_bytes(hash[0..16].try_into().unwrap()); + let threshold = u128::MAX >> (pow_nft.difficulty as u32); + + if hash_value > threshold { + return Err(ContractError::CustomError { + msg: "Proof does not meet difficulty".to_string(), + }); + } + + pow_nft.difficulty = if pow_nft.difficulty >= 2 { + let next_difficulty = (pow_nft.difficulty as f64 * 1.5) as u64; + if next_difficulty > 128 { + return Err(ContractError::CustomError { + msg: format!( + "Max difficulty is 128. Next difficulty will be over 128. Current level: {:?}", + pow_nft.level + ), + }); + } + next_difficulty + } else { + 2 + }; + + pow_nft.level += 1; + + let block_height = ctx.env.block.height; + let nonce = ctx + .env + .transaction + .ok_or_else(|| ContractError::CustomError { + msg: "Transaction info not available".to_string(), + })?; + + let mut hasher = Sha256::new(); + hasher.update(&pow_nft.last_hash); + hasher.update(&solution.to_be_bytes()); + hasher.update(&block_height.to_be_bytes()); + hasher.update(&nonce.index.to_be_bytes()); + let hash = hasher.finalize(); + pow_nft.last_hash = Binary(hash.to_vec()); + + POW_NFT.save(ctx.deps.storage, token_id.clone(), &pow_nft)?; + + Ok(Response::new() + .add_attribute("method", "submit_proof") + .add_attribute("sender", sender) + .add_attribute("token_id", token_id.clone()) + .add_attribute("new_level", pow_nft.level.to_string()) + .add_attribute("new_difficulty", pow_nft.difficulty.to_string()) + .add_event( + Event::new("pow_nft_level_up") + .add_attribute("token_id", token_id) + .add_attribute("new_level", pow_nft.level.to_string()) + .add_attribute("new_difficulty", pow_nft.difficulty.to_string()), + )) +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/interface.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/interface.rs new file mode 100644 index 000000000..0f24711ca --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/interface.rs @@ -0,0 +1,6 @@ +use andromeda_non_fungible_tokens::pow_cw721::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use andromeda_std::{ado_base::MigrateMsg, contract_interface, deploy::ADOMetadata}; + +pub const CONTRACT_ID: &str = "pow-cw721"; + +contract_interface!(PowCw721Contract, CONTRACT_ID, "andromeda_pow_cw721.wasm"); diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/lib.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/lib.rs new file mode 100644 index 000000000..b123a2029 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/lib.rs @@ -0,0 +1,12 @@ +pub mod contract; +pub mod execute; +pub mod query; +pub mod state; + +#[cfg(test)] +pub mod testing; + +#[cfg(not(target_arch = "wasm32"))] +mod interface; +#[cfg(not(target_arch = "wasm32"))] +pub use crate::interface::PowCw721Contract; diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/query.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/query.rs new file mode 100644 index 000000000..f1f26702b --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/query.rs @@ -0,0 +1,25 @@ +use andromeda_non_fungible_tokens::pow_cw721::{GetLinkedCw721AddressResponse, GetPowNFTResponse}; +use andromeda_std::error::ContractError; +use cosmwasm_std::Deps; + +use crate::state::{LINKED_CW721_ADDRESS, POW_NFT}; + +pub fn query_pow_nft(deps: Deps, token_id: String) -> Result { + let pow_nft = POW_NFT + .load(deps.storage, token_id) + .map_err(|_| ContractError::NFTNotFound {})?; + + Ok(GetPowNFTResponse { + nft_response: pow_nft, + }) +} + +pub fn query_linked_cw721_address( + deps: Deps, +) -> Result { + let linked_cw721_address = LINKED_CW721_ADDRESS.load(deps.storage)?; + + Ok(GetLinkedCw721AddressResponse { + linked_cw721_address, + }) +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/state.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/state.rs new file mode 100644 index 000000000..a991b7985 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/state.rs @@ -0,0 +1,6 @@ +use andromeda_non_fungible_tokens::pow_cw721::PowNFTInfo; +use andromeda_std::amp::AndrAddr; +use cw_storage_plus::{Item, Map}; + +pub const LINKED_CW721_ADDRESS: Item = Item::new("linked_cw721_address"); +pub const POW_NFT: Map = Map::new("pow_nft"); diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock.rs new file mode 100644 index 000000000..c0d431b92 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock.rs @@ -0,0 +1,84 @@ +// use andromeda_data_storage::graph::{Coordinate, GetMapInfoResponse, MapInfo}; +use andromeda_non_fungible_tokens::cw721::TokenExtension; +use andromeda_non_fungible_tokens::pow_cw721::{ + ExecuteMsg, GetLinkedCw721AddressResponse, GetPowNFTResponse, InstantiateMsg, QueryMsg, +}; +use andromeda_std::{ + amp::AndrAddr, error::ContractError, testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockStorage}, + Deps, DepsMut, OwnedDeps, Response, +}; + +use crate::contract::{execute, instantiate, query}; +use crate::testing::mock_querier::{mock_dependencies_custom, WasmMockQuerier}; + +pub type MockDeps = OwnedDeps; + +pub fn proper_initialization( + linked_cw721_address: AndrAddr, + authorized_origin_minter_addresses: Option>, +) -> MockDeps { + let mut deps = mock_dependencies_custom(&[]); + let info = mock_info("creator", &[]); + let msg = InstantiateMsg { + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + linked_cw721_address, + authorized_origin_minter_addresses, + }; + let env = mock_env(); + instantiate(deps.as_mut(), env, info, msg).unwrap(); + deps +} + +pub fn mint_pow_nft( + deps: DepsMut<'_>, + sender: &str, + owner: AndrAddr, + token_id: String, + token_uri: Option, + extension: TokenExtension, + base_difficulty: u64, +) -> Result { + let msg = ExecuteMsg::MintPowNFT { + owner, + token_id, + token_uri, + extension, + base_difficulty, + }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn submit_proof( + deps: DepsMut<'_>, + sender: &str, + token_id: String, + solution: u128, +) -> Result { + let msg = ExecuteMsg::SubmitProof { token_id, solution }; + let info = mock_info(sender, &[]); + execute(deps, mock_env(), info, msg) +} + +pub fn query_linked_cw721_address( + deps: Deps, +) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetLinkedCw721Address {}); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} + +pub fn query_pow_nft(deps: Deps, token_id: String) -> Result { + let res = query(deps, mock_env(), QueryMsg::GetPowNFT { token_id }); + match res { + Ok(res) => Ok(from_json(res).unwrap()), + Err(err) => Err(err), + } +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock_querier.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock_querier.rs new file mode 100644 index 000000000..d84dfba91 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mock_querier.rs @@ -0,0 +1,99 @@ +use andromeda_non_fungible_tokens::cw721::QueryMsg as AndrCw721QueryMsg; +use andromeda_std::testing::mock_querier::MockAndromedaQuerier; +use andromeda_std::{ + ado_base::InstantiateMsg, ado_contract::ADOContract, + testing::mock_querier::MOCK_KERNEL_CONTRACT, +}; +use cosmwasm_std::QuerierWrapper; +use cosmwasm_std::{ + from_json, + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}, + Coin, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, +}; +use cosmwasm_std::{to_json_binary, Binary, ContractResult}; + +pub const MOCK_CW721_CONTRACT: &str = "cw721_contract"; +pub const ORIGIN_MINTER: &str = "origin_minter"; + +/// Alternative to `cosmwasm_std::testing::mock_dependencies` that allows us to respond to custom queries. +/// +/// Automatically assigns a kernel address as MOCK_KERNEL_CONTRACT. +pub fn mock_dependencies_custom( + contract_balance: &[Coin], +) -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])); + let storage = MockStorage::default(); + let mut deps = OwnedDeps { + storage, + api: MockApi::default(), + querier: custom_querier, + custom_query_type: std::marker::PhantomData, + }; + ADOContract::default() + .instantiate( + &mut deps.storage, + mock_env(), + &deps.api, + &QuerierWrapper::new(&deps.querier), + mock_info("sender", &[]), + InstantiateMsg { + ado_type: "pow-cw721".to_string(), + ado_version: "test".to_string(), + kernel_address: MOCK_KERNEL_CONTRACT.to_string(), + owner: None, + }, + ) + .unwrap(); + deps +} +pub struct WasmMockQuerier { + pub base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_json(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {e}"), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + match contract_addr.as_str() { + MOCK_CW721_CONTRACT => self.handle_cw721_smart_query(msg), + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + _ => MockAndromedaQuerier::default().handle_query(&self.base, request), + } + } + + fn handle_cw721_smart_query(&self, msg: &Binary) -> QuerierResult { + match from_json(msg).unwrap() { + AndrCw721QueryMsg::OwnerOf { .. } => { + let msg_response = cw721::OwnerOfResponse { + owner: ORIGIN_MINTER.to_string(), + approvals: vec![], + }; + SystemResult::Ok(ContractResult::Ok(to_json_binary(&msg_response).unwrap())) + } + _ => panic!("Unsupported Query"), + } + } + + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { base } + } +} diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mod.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mod.rs new file mode 100644 index 000000000..217ceb8c1 --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/mod.rs @@ -0,0 +1,3 @@ +mod mock; +mod mock_querier; +mod tests; diff --git a/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/tests.rs b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/tests.rs new file mode 100644 index 000000000..6405650ee --- /dev/null +++ b/contracts/non-fungible-tokens/andromeda-pow-cw721/src/testing/tests.rs @@ -0,0 +1,171 @@ +use andromeda_non_fungible_tokens::cw721::{QueryMsg as AndrCw721QueryMsg, TokenExtension}; +use andromeda_std::{amp::AndrAddr, common::encode_binary, error::ContractError}; +use cosmwasm_std::Querier; +use cosmwasm_std::{from_json, to_json_binary, QueryRequest, WasmQuery}; +use test_case::test_case; + +use crate::testing::mock::{ + mint_pow_nft, proper_initialization, query_linked_cw721_address, query_pow_nft, submit_proof, +}; +use crate::testing::mock_querier::{MOCK_CW721_CONTRACT, ORIGIN_MINTER}; + +pub const AUTHORIZED_ORIGIN_MINTER1: &str = "authorized_origin_minter1"; +pub const AUTHORIZED_ORIGIN_MINTER2: &str = "authorized_origin_minter2"; +pub const UNAUTHORIZED_ORIGIN_MINTER: &str = "unauthorized_origin_minter"; + +#[test] +fn test_instantiation() { + let deps = proper_initialization(AndrAddr::from_string(MOCK_CW721_CONTRACT), None); + let linked_cw721_address = query_linked_cw721_address(deps.as_ref()) + .unwrap() + .linked_cw721_address; + assert_eq!( + linked_cw721_address, + AndrAddr::from_string(MOCK_CW721_CONTRACT) + ); +} + +#[test] +fn test_mint_pow_nft_invalid_user() { + let mut deps = proper_initialization( + AndrAddr::from_string(MOCK_CW721_CONTRACT), + Some(vec![ + AndrAddr::from_string(AUTHORIZED_ORIGIN_MINTER1), + AndrAddr::from_string(AUTHORIZED_ORIGIN_MINTER2), + ]), + ); + let err_response = mint_pow_nft( + deps.as_mut(), + UNAUTHORIZED_ORIGIN_MINTER, + AndrAddr::from_string("owner"), + "test_pow1".to_string(), + None, + TokenExtension { + publisher: "Andromeda".to_string(), + }, + 10_u64, + ) + .unwrap_err(); + + assert_eq!(err_response, ContractError::Unauthorized {}); +} + +#[test] +fn test_mint_pow_nft() { + let mut deps = proper_initialization(AndrAddr::from_string(MOCK_CW721_CONTRACT), None); + + mint_pow_nft( + deps.as_mut(), + ORIGIN_MINTER, + AndrAddr::from_string(ORIGIN_MINTER), + "test_pow1".to_string(), + None, + TokenExtension { + publisher: "Andromeda".to_string(), + }, + 10_u64, + ) + .unwrap(); + + let owner_query_msg = to_json_binary(&QueryRequest::::Wasm( + WasmQuery::Smart { + contract_addr: MOCK_CW721_CONTRACT.to_string(), + msg: encode_binary(&AndrCw721QueryMsg::OwnerOf { + token_id: "test_pow1".to_string(), + include_expired: None, + }) + .unwrap(), + }, + )) + .unwrap(); + + let raw_query_res = deps.querier.raw_query(&owner_query_msg); + let owner_response: cw721::OwnerOfResponse = + from_json(&(raw_query_res.unwrap()).unwrap()).unwrap(); + assert_eq!(owner_response.owner, ORIGIN_MINTER); + + let pow_nft = query_pow_nft(deps.as_ref(), "test_pow1".to_string()).unwrap(); + assert_eq!(pow_nft.nft_response.level, 1); +} + +#[test_case("test_pow1", 20_u64, 582586_u128 ; "Difficulty: 20")] +#[test_case("test_pow1", 10_u64, 944_u128 ; "Difficulty: 10")] +#[test_case("test_pow1", 2_u64, 19_u128 ; "Difficulty: 2")] +fn test_submit_valid_proofs(token_id: &str, difficulty: u64, nonce: u128) { + let mut deps = proper_initialization(AndrAddr::from_string(MOCK_CW721_CONTRACT), None); + + mint_pow_nft( + deps.as_mut(), + ORIGIN_MINTER, + AndrAddr::from_string(ORIGIN_MINTER), + token_id.to_string(), + None, + TokenExtension { + publisher: "Andromeda".to_string(), + }, + difficulty, + ) + .unwrap(); + + submit_proof(deps.as_mut(), "viewer", "test_pow1".to_string(), nonce).unwrap(); + + let pow_nft = query_pow_nft(deps.as_ref(), token_id.to_string()).unwrap(); + + assert_eq!(2, pow_nft.nft_response.level); +} + +#[test_case("test_pow1", 20_u64, 58256_u128 ; "Difficulty: 20")] +#[test_case("test_pow1", 10_u64, 94_u128 ; "Difficulty: 10")] +#[test_case("test_pow1", 2_u64, 10_u128 ; "Difficulty: 2")] +fn test_submit_invalid_proofs(token_id: &str, difficulty: u64, nonce: u128) { + let mut deps = proper_initialization(AndrAddr::from_string(MOCK_CW721_CONTRACT), None); + + mint_pow_nft( + deps.as_mut(), + ORIGIN_MINTER, + AndrAddr::from_string(ORIGIN_MINTER), + token_id.to_string(), + None, + TokenExtension { + publisher: "Andromeda".to_string(), + }, + difficulty, + ) + .unwrap(); + + let err = submit_proof(deps.as_mut(), "viewer", "test_pow1".to_string(), nonce).unwrap_err(); + + assert_eq!( + err, + ContractError::CustomError { + msg: "Proof does not meet difficulty".to_string() + } + ); +} + +#[test] +fn test_increase_level() { + let mut deps = proper_initialization(AndrAddr::from_string(MOCK_CW721_CONTRACT), None); + + mint_pow_nft( + deps.as_mut(), + ORIGIN_MINTER, + AndrAddr::from_string(ORIGIN_MINTER), + "test_pow1".to_string(), + None, + TokenExtension { + publisher: "Andromeda".to_string(), + }, + 2_u64, + ) + .unwrap(); + + let nonces_to_submit = vec![19_u128, 5_u128, 0_u128, 50_u128, 1474_u128, 16440_u128]; + + for nonce in nonces_to_submit.iter() { + submit_proof(deps.as_mut(), "viewer", "test_pow1".to_string(), *nonce).unwrap(); + } + + let pow_nft = query_pow_nft(deps.as_ref(), "test_pow1".to_string()).unwrap(); + assert_eq!(7, pow_nft.nft_response.level); +} diff --git a/packages/andromeda-non-fungible-tokens/src/lib.rs b/packages/andromeda-non-fungible-tokens/src/lib.rs index 74ec1fbae..a86275a9e 100644 --- a/packages/andromeda-non-fungible-tokens/src/lib.rs +++ b/packages/andromeda-non-fungible-tokens/src/lib.rs @@ -1,6 +1,7 @@ pub mod auction; pub mod crowdfund; pub mod cw721; +pub mod pow_cw721; // pub mod cw721_bid; // pub mod cw721_staking; // pub mod cw721_timelock; diff --git a/packages/andromeda-non-fungible-tokens/src/pow_cw721.rs b/packages/andromeda-non-fungible-tokens/src/pow_cw721.rs new file mode 100644 index 000000000..59f4719dd --- /dev/null +++ b/packages/andromeda-non-fungible-tokens/src/pow_cw721.rs @@ -0,0 +1,56 @@ +use andromeda_std::{amp::AndrAddr, andr_exec, andr_instantiate, andr_query}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary}; + +use crate::cw721::TokenExtension; + +#[andr_instantiate] +#[cw_serde] +pub struct InstantiateMsg { + pub linked_cw721_address: AndrAddr, + pub authorized_origin_minter_addresses: Option>, +} + +#[andr_exec] +#[cw_serde] +pub enum ExecuteMsg { + MintPowNFT { + owner: AndrAddr, + token_id: String, + token_uri: Option, + extension: TokenExtension, + base_difficulty: u64, + }, + SubmitProof { + token_id: String, + solution: u128, + }, +} + +#[cw_serde] +pub struct PowNFTInfo { + pub owner: Addr, + pub level: u64, + pub last_hash: Binary, + pub difficulty: u64, +} + +#[andr_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(GetPowNFTResponse)] + GetPowNFT { token_id: String }, + #[returns(GetLinkedCw721AddressResponse)] + GetLinkedCw721Address {}, +} + +#[cw_serde] +pub struct GetPowNFTResponse { + pub nft_response: PowNFTInfo, +} + +#[cw_serde] +pub struct GetLinkedCw721AddressResponse { + pub linked_cw721_address: AndrAddr, +}