diff --git a/docs/tutorial/04-project.md b/docs/tutorial/04-project.md index 641299ce..2e3d9ccd 100644 --- a/docs/tutorial/04-project.md +++ b/docs/tutorial/04-project.md @@ -21,19 +21,17 @@ Edit Cargo.toml to have the following contract dependencies ```toml [dependencies] -cosmwasm-schema = "1.1.9" -cosmwasm-std = "1.1.9" -cosmwasm-storage = "1.1.9" -cw-storage-plus = "1.0.1" +cosmwasm-schema = "2.1.4" +cosmwasm-std = "2.1.4" +cw-storage-plus = "2.0.0" cw2 = "1.0.1" -provwasm-std = "1.1.2" -schemars = "0.8.10" -serde = { version = "1.0.145", default-features = false, features = ["derive"] } -thiserror = { version = "1.0.31" } +provwasm-std = "2.5.0" +schemars = "0.8.16" +serde = { version = "1.0.197", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.58" } [dev-dependencies] -cw-multi-test = "0.16.2" -provwasm-mocks = "1.1.2" +provwasm-mocks = "2.5.0" ``` Reset the README and clear out the current JSON schema artifacts. diff --git a/docs/tutorial/05-requirements.md b/docs/tutorial/05-requirements.md index 5aba796c..f1d262e0 100644 --- a/docs/tutorial/05-requirements.md +++ b/docs/tutorial/05-requirements.md @@ -28,9 +28,9 @@ The tutorial contract requires the following handler functions and messages. ## Instantiate ```rust -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn instantiate( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: InitMsg, @@ -62,9 +62,9 @@ The following validations must be performed during initialization. ## Execute ```rust -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn execute( - deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -97,10 +97,10 @@ action when a purchase has been completed. ## Query ```rust -#[cfg_attr(not(feature = "library"), entry_point)] +#[entry_point] pub fn query( - deps: Deps, - env: Env, + deps: Deps, + _env: Env, // NOTE: A '_' prefix indicates a variable is unused (suppress linter warnings) msg: QueryMsg, ) -> ... ``` diff --git a/docs/tutorial/06-develop.md b/docs/tutorial/06-develop.md index b45bd2ea..c74b962e 100644 --- a/docs/tutorial/06-develop.md +++ b/docs/tutorial/06-develop.md @@ -3,7 +3,9 @@ ## Development In this section we will build and test the functionality of the smart contract defined in the -[Requirements](05-requirements.md) section. Replace the contents of the files generated from template with the the code listed in this section. The best way to learn is to type out the code. But, it is completely acceptable to copy and paste as well. +[Requirements](05-requirements.md) section. Replace the contents of the files generated from template with the the code +listed in this section. The best way to learn is to type out the code. But, it is completely acceptable to copy and +paste as well. ### Setup @@ -15,13 +17,18 @@ File: `Makefile` .PHONY: all all: fmt build test lint schema optimize +.PHONY: pre-optimize +pre-optimize: fmt build test lint schema + +UNAME_M := $(shell uname -m) + .PHONY: fmt fmt: @cargo fmt --all -- --check .PHONY: build build: - @cargo wasm + @cargo build .PHONY: test test: @@ -29,11 +36,16 @@ test: .PHONY: lint lint: - @cargo clippy -- -D warnings + @cargo clippy .PHONY: schema schema: @cargo schema + +.PHONY: clean +clean: + @cargo clean + @cargo clean --target-dir artifacts ``` NOTE: A few of these cargo commands are aliases. The full commands can be seen in the @@ -41,9 +53,9 @@ NOTE: A few of these cargo commands are aliases. The full commands can be seen i ```toml [alias] -wasm = "build --release --target wasm32-unknown-unknown" +wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --bin schema" +schema = "run --example schema" ``` ### Library @@ -58,8 +70,6 @@ pub mod contract; mod error; pub mod msg; pub mod state; - -pub use crate::error::ContractError; ``` ### Errors @@ -69,16 +79,16 @@ File: `src/error.rs` Adds customizable errors to the smart contract. ```rust -use cosmwasm_std::StdError; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Unauthorized")] - Unauthorized {}, +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, } ``` @@ -89,25 +99,25 @@ File: `src/state.rs` Defines a singleton (one key, one value) configuration state for the smart contract. ```rust -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use cosmwasm_std::{Addr, Decimal}; -use cw_storage_plus::Item; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct Config { - // The required purchase denomination - pub purchase_denom: String, - // The merchant account - pub merchant_address: Addr, - // The fee collection account - pub fee_collection_address: Addr, - // The percentage to collect on transfers - pub fee_percent: Decimal, -} - -pub const CONFIG: Item = Item::new("config"); +use cosmwasm_std::{Addr, Decimal}; +use cw_storage_plus::Item; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub const CONFIG: Item = Item::new("config"); + +/// Fields that comprise the smart contract state +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct State { + // The required purchase denomination + pub purchase_denom: String, + // The merchant account + pub merchant_address: Addr, + // The fee collection account + pub fee_collection_address: Addr, + // The percentage to collect on transfers + pub fee_percent: Decimal, +} ``` ### Messages @@ -117,41 +127,34 @@ File: `src/msg.rs` Define message types for the smart contract. ```rust -use crate::state::Config; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Decimal; - -/// A message sent to initialize the contract state. -#[cw_serde] -pub struct InstantiateMsg { - pub contract_name: String, - pub purchase_denom: String, - pub merchant_address: String, - pub fee_percent: Decimal, -} - -/// A message sent to transfer funds and collect fees for a purchase. -#[cw_serde] -pub enum ExecuteMsg { - Purchase { id: String }, -} - -/// A message sent to migrate the contract to a new code id. -#[cw_serde] -pub enum MigrateMsg {} - -/// A message sent to query contract config state. -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(ConfigResponse)] - QueryRequest {}, -} - -/// A response . -#[cw_serde] -pub struct ConfigResponse { - pub config: Config, +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Decimal; + +/// A message sent to initialize the contract state. +#[cw_serde] +pub struct InitMsg { + pub contract_name: String, + pub purchase_denom: String, + pub merchant_address: String, + pub fee_percent: Decimal, +} + +/// A message sent to transfer funds and collect fees for a purchase. +#[cw_serde] +pub enum ExecuteMsg { + Purchase { id: String }, +} + +/// Migrate the contract. +#[cw_serde] +pub struct MigrateMsg {} + +/// A message sent to query contract config state. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(crate::state::State)] + QueryRequest {}, } ``` @@ -164,79 +167,91 @@ File: `src/contract.rs` The following imports are required for the init, query and handle functions. ```rust -use cosmwasm_std::{ - coin, entry_point, to_binary, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, - MessageInfo, Response, StdError, StdResult, -}; -use cw2::set_contract_version; -use provwasm_std::{bind_name, NameBinding, ProvenanceMsg, ProvenanceQuery}; -use std::ops::Mul; - -use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{Config, CONFIG}; +use cosmwasm_std::{ + coin, entry_point, to_json_binary, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Response, StdError, StdResult, +}; +use provwasm_std::types::provenance::name::v1::{MsgBindNameRequest, NameRecord}; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; +use crate::state::{State, CONFIG}; ``` #### Instantiate Handler code for contract instantiation. -```rust -// version info for migration info -const CONTRACT_NAME: &str = "crates.io:tutorial"; -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); - -#[entry_point] -pub fn instantiate( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, -) -> Result, ContractError> { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - // Ensure no funds were sent with the message - if !info.funds.is_empty() { - let err = "purchase funds are not allowed to be sent during init"; - return Err(ContractError::Std(StdError::generic_err(err))); - } - - // Ensure there are limits on fees. - if msg.fee_percent.is_zero() || msg.fee_percent > Decimal::percent(25) { - return Err(ContractError::Std(StdError::generic_err( - "fee percent must be > 0.0 and <= 0.25", - ))); - } - - // Ensure the merchant address is not also the fee collection address - if msg.merchant_address == info.sender { - return Err(ContractError::Std(StdError::generic_err( - "merchant address can't be the fee collection address", - ))); - } - - // Create and save contract config state. The fee collection address represents the network - // (ie they get paid fees), thus they must be the message sender. let merchant_address = deps.api.addr_validate(&msg.merchant_address)?; - CONFIG.save( - deps.storage, - &Config { - purchase_denom: msg.purchase_denom, - merchant_address, - fee_collection_address: info.sender, - fee_percent: msg.fee_percent, - }, - )?; - - // Create a message that will bind a restricted name to the contract address. - let msg = bind_name( - &msg.contract_name, - env.contract.address, - NameBinding::Restricted, - )?; - - Ok(Response::new() - .add_message(msg) - .add_attribute("action", "init")) +```rust +#[entry_point] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InitMsg, +) -> Result { + // Ensure no funds were sent with the message + if !info.funds.is_empty() { + let err = "purchase funds are not allowed to be sent during init"; + return Err(StdError::generic_err(err)); + } + + // Ensure there are limits on fees. + if msg.fee_percent.is_zero() || msg.fee_percent > Decimal::percent(25) { + return Err(StdError::generic_err( + "fee percent must be > 0.0 and <= 0.25", + )); + } + + // Ensure the merchant address is not also the fee collection address + if msg.merchant_address.eq(&info.sender.to_string()) { + return Err(StdError::generic_err( + "merchant address can't be the fee collection address", + )); + } + + // Create and save contract config state. The fee collection address represents the network + // (ie they get paid fees), thus they must be the message sender. + let merchant_address = deps.api.addr_validate(&msg.merchant_address)?; + CONFIG.save( + deps.storage, + &State { + purchase_denom: msg.purchase_denom, + merchant_address, + fee_collection_address: info.sender, + fee_percent: msg.fee_percent, + }, + )?; + + // Create a message that will bind a restricted name to the contract address. + let split: Vec<&str> = msg.contract_name.splitn(2, '.').collect(); + let record = split.first(); + let parent = split.last(); + + match (parent, record) { + (Some(parent), Some(record)) => { + // Create a bind name message + let bind_name_msg = MsgBindNameRequest { + parent: Some(NameRecord { + name: parent.to_string(), + address: env.contract.address.to_string(), + restricted: true, + }), + record: Some(NameRecord { + name: record.to_string(), + address: env.contract.address.to_string(), + restricted: true, + }), + }; + + // Dispatch bind name message and add event attributes. + let res = Response::new() + .add_message(bind_name_msg) + .add_attribute("action", "init"); + Ok(res) + } + (_, _) => Err(StdError::generic_err("Invalid contract name")), + } } ``` @@ -245,92 +260,93 @@ pub fn instantiate( Query code for accessing contract state. ```rust -#[entry_point] -pub fn query( - deps: Deps, - _env: Env, // NOTE: A '_' prefix indicates a variable is unused (suppress linter warnings) - msg: QueryMsg, -) -> StdResult { - match msg { - QueryMsg::QueryRequest {} => { - let state = CONFIG.load(deps.storage)?; - let json = to_binary(&state)?; - Ok(json) - } - } +#[entry_point] +pub fn query( + deps: Deps, + _env: Env, // NOTE: A '_' prefix indicates a variable is unused (suppress linter warnings) + msg: QueryMsg, +) -> StdResult { + match msg { + QueryMsg::QueryRequest {} => { + let state = CONFIG.load(deps.storage)?; + let json = to_json_binary(&state)?; + Ok(json) + } + } } ``` #### Execute ```rust -#[entry_point] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result, ContractError> { - match msg { - ExecuteMsg::Purchase { id } => try_purchase(deps, env, info, id), - } -} - -// Calculates transfers and fees, then dispatches messages to the bank module. -fn try_purchase( - deps: DepsMut, - env: Env, - info: MessageInfo, - id: String, -) -> Result, ContractError> { - // Ensure funds were sent with the message - if info.funds.is_empty() { - let err = "no purchase funds sent"; - return Err(ContractError::Std(StdError::generic_err(err))); - } - - // Load state - let state = CONFIG.load(deps.storage)?; - let fee_pct = state.fee_percent; - - // Ensure the funds have the required amount and denomination - for funds in info.funds.iter() { - if funds.amount.is_zero() || funds.denom != state.purchase_denom { - let err = format!("invalid purchase funds: {}{}", funds.amount, funds.denom); - return Err(ContractError::Std(StdError::generic_err(err))); - } - } - - // Calculate amounts and create bank transfers to the merchant account - let transfers = CosmosMsg::Bank(BankMsg::Send { - to_address: state.merchant_address.to_string(), - amount: info - .funds - .iter() - .map(|sent| { - let fees = sent.amount.mul(fee_pct).u128(); - coin(sent.amount.u128() - fees, sent.denom.clone()) - }) - .collect(), - }); - - // Calculate fees and create bank transfers to the fee collection account - let fees = CosmosMsg::Bank(BankMsg::Send { - to_address: state.fee_collection_address.to_string(), - amount: info - .funds - .iter() - .map(|sent| coin(sent.amount.mul(fee_pct).u128(), sent.denom.clone())) - .collect(), - }); +#[entry_point] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + // BankMsg + match msg { + ExecuteMsg::Purchase { id } => try_purchase(deps, env, info, id), + } +} - // Return a response that will dispatch the transfers to the bank module and emit events. - Ok(Response::new() - .add_message(transfers) - .add_message(fees) - .add_attribute("action", "purchase") - .add_attribute("purchase_id", id) - .add_attribute("purchase_time", env.block.time.to_string())) +// Calculates transfers and fees, then dispatches messages to the bank module. +fn try_purchase( + deps: DepsMut, + env: Env, + info: MessageInfo, + id: String, +) -> Result { + // Ensure funds were sent with the message + if info.funds.is_empty() { + let err = "no purchase funds sent"; + return Err(ContractError::Std(StdError::generic_err(err))); + } + + // Load state + let state = CONFIG.load(deps.storage)?; + let fee_pct = state.fee_percent; + + // Ensure the funds have the required amount and denomination + for funds in info.funds.iter() { + if funds.amount.is_zero() || funds.denom != state.purchase_denom { + let err = format!("invalid purchase funds: {}{}", funds.amount, funds.denom); + return Err(ContractError::Std(StdError::generic_err(err))); + } + } + + // Calculate amounts and create bank transfers to the merchant account + let transfers = CosmosMsg::Bank(BankMsg::Send { + to_address: state.merchant_address.to_string(), + amount: info + .funds + .iter() + .map(|sent| { + let fees = sent.amount.mul_floor(fee_pct).u128(); + coin(sent.amount.u128() - fees, sent.denom.clone()) + }) + .collect(), + }); + + // Calculate fees and create bank transfers to the fee collection account + let fees = CosmosMsg::Bank(BankMsg::Send { + to_address: state.fee_collection_address.to_string(), + amount: info + .funds + .iter() + .map(|sent| coin(sent.amount.mul_floor(fee_pct).u128(), sent.denom.clone())) + .collect(), + }); + + // Return a response that will dispatch the transfers to the bank module and emit events. + Ok(Response::new() + .add_message(transfers) + .add_message(fees) + .add_attribute("action", "purchase") + .add_attribute("purchase_id", id) + .add_attribute("purchase_time", env.block.time.to_string())) } ``` @@ -355,272 +371,283 @@ File: `src/contract.rs` Add an inner module with imports for contract unit tests ```rust -#[cfg(test)] -mod tests { - use super::*; - use crate::state::Config; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{from_binary, Addr}; - use provwasm_mocks::mock_dependencies; - use provwasm_std::{NameMsgParams, ProvenanceMsgParams}; - - #[test] - fn valid_init() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create valid config state - let res = instantiate( - deps.as_mut(), - mock_env(), - mock_info("feebucket", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(10), - }, - ) - .unwrap(); - - // Ensure a message was created to bind the name to the contract address. - assert_eq!(res.messages.len(), 1); - match &res.messages[0].msg { - CosmosMsg::Custom(msg) => match &msg.params { - ProvenanceMsgParams::Name(p) => match &p { - NameMsgParams::BindName { name, .. } => assert_eq!(name, "tutorial.sc.pb"), - _ => panic!("unexpected name params"), - }, - _ => panic!("unexpected provenance params"), - }, - _ => panic!("unexpected cosmos message"), - } - } - - #[test] - fn invalid_merchant_init() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create an invalid init message - let err = instantiate( - deps.as_mut(), - mock_env(), - mock_info("merchant", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(10), - }, - ) - .unwrap_err(); - - // Ensure the expected error was returned. - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, "merchant address can't be the fee collection address") - } - _ => panic!("unexpected init error"), - } - } - - #[test] - fn invalid_fee_percent_init() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create an invalid init message. - let err = instantiate( - deps.as_mut(), - mock_env(), - mock_info("feebucket", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(37), // error: > 25% - }, - ) - .unwrap_err(); - - // Ensure the expected error was returned - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, "fee percent must be > 0.0 and <= 0.25") - } - _ => panic!("unexpected init error"), - } - } - - #[test] - fn query_test() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create config state - instantiate( - deps.as_mut(), - mock_env(), - mock_info("feebucket", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(10), - }, - ) - .unwrap(); // Panics on error - - // Call the smart contract query function to get stored state. - let bin = query(deps.as_ref(), mock_env(), QueryMsg::QueryRequest {}).unwrap(); - let resp: Config = from_binary(&bin).unwrap(); - - // Ensure the expected init fields were properly stored. - assert_eq!(resp.merchant_address, Addr::unchecked("merchant")); - assert_eq!(resp.purchase_denom, "purchasecoin"); - assert_eq!(resp.fee_collection_address, Addr::unchecked("feebucket")); - assert_eq!(resp.fee_percent, Decimal::percent(10)); - } - - #[test] - fn handle_valid_purchase() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create config state - instantiate( - deps.as_mut(), - mock_env(), - mock_info("feebucket", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(10), - }, - ) - .unwrap(); - - // Send a valid purchase message of 100purchasecoin - let res = execute( - deps.as_mut(), - mock_env(), - mock_info("consumer", &[coin(100, "purchasecoin")]), - ExecuteMsg::Purchase { - id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(), - }, - ) - .unwrap(); - - // Ensure we have the merchant transfer and fee collection bank messages - assert_eq!(res.messages.len(), 2); - - // Ensure we got the proper bank transfer values. - // 10% fees on 100 purchasecoin => 90 purchasecoin for the merchant and 10 purchasecoin for the fee bucket. - let expected_transfer = coin(90, "purchasecoin"); - let expected_fees = coin(10, "purchasecoin"); - res.messages.into_iter().for_each(|msg| match msg.msg { - CosmosMsg::Bank(BankMsg::Send { - amount, to_address, .. - }) => { - assert_eq!(amount.len(), 1); - if to_address == "merchant" { - assert_eq!(amount[0], expected_transfer) - } else if to_address == "feebucket" { - assert_eq!(amount[0], expected_fees) - } else { - panic!("unexpected to_address in bank message") - } - } - _ => panic!("unexpected message type"), - }); - - // Ensure we got the purchase ID event attribute value - let expected_purchase_id = "a7918172-ac09-43f6-bc4b-7ac2fbad17e9"; - res.attributes.into_iter().for_each(|atr| { - if atr.key == "purchase_id" { - assert_eq!(atr.value, expected_purchase_id) - } - }) - } - - #[test] - fn handle_invalid_funds() { - // Create mocks - let mut deps = mock_dependencies(&[]); - - // Create config state - instantiate( - deps.as_mut(), - mock_env(), - mock_info("feebucket", &[]), - InstantiateMsg { - contract_name: "tutorial.sc.pb".into(), - purchase_denom: "purchasecoin".into(), - merchant_address: Addr::unchecked("merchant"), - fee_percent: Decimal::percent(10), - }, - ) - .unwrap(); - - // Don't send any funds - let err = execute( - deps.as_mut(), - mock_env(), - mock_info("consumer", &[]), - ExecuteMsg::Purchase { - id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(), - }, - ) - .unwrap_err(); - - // Ensure the expected error was returned. - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, "no purchase funds sent") - } - _ => panic!("unexpected handle error"), - } - - // Send zero amount for a valid denom - let err = execute( - deps.as_mut(), - mock_env(), - mock_info("consumer", &[coin(0, "purchasecoin")]), - ExecuteMsg::Purchase { - id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(), - }, - ) - .unwrap_err(); - - // Ensure the expected error was returned. - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, "invalid purchase funds: 0purchasecoin") - } - _ => panic!("unexpected handle error"), - } - - // Send invalid denom - let err = execute( - deps.as_mut(), - mock_env(), - mock_info("consumer", &[coin(100, "fakecoin")]), - ExecuteMsg::Purchase { - id: "a7918172-ac09-43f6-bc4b-7ac2fbad17e9".into(), - }, - ) - .unwrap_err(); - - // Ensure the expected error was returned. - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, "invalid purchase funds: 100fakecoin") - } - _ => panic!("unexpected handle error"), - } - } +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{message_info, mock_env}; + use cosmwasm_std::{from_json, Addr, AnyMsg, Binary, CosmosMsg}; + use provwasm_mocks::mock_provenance_dependencies; + use provwasm_std::types::provenance::name::v1::{ + QueryResolveRequest, QueryResolveResponse, QueryReverseLookupRequest, + QueryReverseLookupResponse, + }; + + #[test] + fn init_test() { + // Create default provenance mocks. + let mut deps = mock_provenance_dependencies(); + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + + // Give the contract a name + let msg = InitMsg { + name: "contract.pb".into(), + }; + + let contract_address = env.contract.address.to_string(); + + // Ensure a message was created to bind the name to the contract address. + let res = instantiate(deps.as_mut(), env, info, msg).unwrap(); + assert_eq!(1, res.messages.len()); + + match &res.messages[0].msg { + CosmosMsg::Any(AnyMsg { type_url, value }) => { + let expected: Binary = MsgBindNameRequest { + parent: Some(NameRecord { + name: "pb".to_string(), + address: contract_address.clone(), + restricted: true, + }), + record: Some(NameRecord { + name: "contract".to_string(), + address: contract_address, + restricted: true, + }), + } + .into(); + + assert_eq!(type_url, "/provenance.name.v1.MsgBindNameRequest"); + assert_eq!(value, &expected) + } + _ => panic!("unexpected cosmos message"), + } + } + + #[test] + fn bind_name_success() { + // Init state + let mut deps = mock_provenance_dependencies(); + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = InitMsg { + name: "contract.pb".into(), + }; + let _ = instantiate(deps.as_mut(), env, info, msg).unwrap(); // Panics on error + + // Bind a name + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = ExecuteMsg::BindPrefix { + prefix: "test".into(), + }; + + let contract_address = env.contract.address.to_string(); + + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + + // Assert the correct message was created + match &res.messages[0].msg { + CosmosMsg::Any(AnyMsg { type_url, value }) => { + let expected: Binary = MsgBindNameRequest { + parent: Some(NameRecord { + name: "contract.pb".to_string(), + address: contract_address.clone(), + restricted: true, + }), + record: Some(NameRecord { + name: "test".to_string(), + address: contract_address, + restricted: true, + }), + } + .into(); + + assert_eq!(type_url, "/provenance.name.v1.MsgBindNameRequest"); + assert_eq!(value, &expected) + } + _ => panic!("unexpected cosmos message"), + } + } + + #[test] + fn unbind_name_success() { + // Init state + let mut deps = mock_provenance_dependencies(); + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = InitMsg { + name: "contract.pb".into(), + }; + let _ = instantiate(deps.as_mut(), env, info, msg).unwrap(); // Panics on error + + // Bind a name + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = ExecuteMsg::UnbindPrefix { + prefix: "test".into(), + }; + + let contract_address = env.contract.address.to_string(); + + let res = execute(deps.as_mut(), env, info, msg).unwrap(); + + // Assert the correct message was created + assert_eq!(1, res.messages.len()); + match &res.messages[0].msg { + CosmosMsg::Any(AnyMsg { type_url, value }) => { + let expected: Binary = MsgDeleteNameRequest { + record: Some(NameRecord { + name: "test.contract.pb".to_string(), + address: contract_address, + restricted: true, + }), + } + .into(); + + assert_eq!(type_url, "/provenance.name.v1.MsgDeleteNameRequest"); + assert_eq!(value, &expected) + } + _ => panic!("unexpected cosmos message"), + } + } + + #[test] + fn bind_name_unauthorized() { + // Init state + let mut deps = mock_provenance_dependencies(); + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = InitMsg { + name: "contract.pb".into(), + }; + let _ = instantiate(deps.as_mut(), env, info, msg).unwrap(); // Panics on error + + // Try to bind a name with some other sender address + let env = mock_env(); + let info = message_info(&Addr::unchecked("other"), &[]); // error: not 'sender' + let msg = ExecuteMsg::BindPrefix { + prefix: "test".into(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + + // Assert an unauthorized error was returned + match err { + ContractError::Unauthorized {} => {} + e => panic!("unexpected error: {:?}", e), + } + } + + #[test] + fn unbind_name_unauthorized() { + // Init state + let mut deps = mock_provenance_dependencies(); + let env = mock_env(); + let info = message_info(&Addr::unchecked("sender"), &[]); + let msg = InitMsg { + name: "contract.pb".into(), + }; + let _ = instantiate(deps.as_mut(), env, info, msg).unwrap(); // Panics on error + + // Try to bind a name with some other sender address + let env = mock_env(); + let info = message_info(&Addr::unchecked("other"), &[]); // error: not 'sender' + let msg = ExecuteMsg::UnbindPrefix { + prefix: "test".into(), + }; + let err = execute(deps.as_mut(), env, info, msg).unwrap_err(); + + // Assert an unauthorized error was returned + match err { + ContractError::Unauthorized {} => {} + e => panic!("unexpected error: {:?}", e), + } + } + + #[test] + fn query_resolve() { + // Create provenance mock deps with a single bound name. + + let mut deps = mock_provenance_dependencies(); + + let mock_response = QueryResolveResponse { + address: "tp1y0txdp3sqmxjvfdaa8hfvwcljl8ugcfv26uync".to_string(), + restricted: false, + }; + + QueryResolveRequest::mock_response(&mut deps.querier, mock_response); + + // Call the smart contract query function to resolve the address for our test name. + let bin = query( + deps.as_ref(), + mock_env(), + QueryMsg::Resolve { + name: "a.pb".into(), + }, + ) + .unwrap(); + + // Ensure that we got the expected address. + let rep: String = from_json(bin).unwrap(); + assert_eq!(rep, "tp1y0txdp3sqmxjvfdaa8hfvwcljl8ugcfv26uync") + } + + #[test] + fn query_lookup() { + // Create provenance mock deps with two bound names. + let mut deps = mock_provenance_dependencies(); + + let mock_response = QueryReverseLookupResponse { + name: vec!["b.pb".to_string(), "a.pb".to_string()], + pagination: None, + }; + + QueryReverseLookupRequest::mock_response(&mut deps.querier, mock_response.clone()); + + // Call the smart contract query function to lookup names bound to an address. + let bin = query( + deps.as_ref(), + mock_env(), + QueryMsg::Lookup { + address: deps.api.addr_make("address").into(), + }, + ) + .unwrap(); + + // Ensure that we got the expected number of records. + let rep: LookupResponse = from_json(bin).unwrap(); + assert_eq!( + rep, + LookupResponse { + name: vec!["b.pb".to_string(), "a.pb".to_string()] + } + ); + } + + #[test] + fn query_lookup_empty() { + // Create provenance mock deps with a bound name. + let mut deps = mock_provenance_dependencies(); + let mock_response = QueryReverseLookupResponse { + name: vec![], + pagination: None, + }; + + QueryReverseLookupRequest::mock_response(&mut deps.querier, mock_response.clone()); + + // Call the smart contract query function to lookup names bound to an address. + let bin = query( + deps.as_ref(), + mock_env(), + QueryMsg::Lookup { + address: deps.api.addr_make("address2").into(), + }, + ) + .unwrap(); + + // Ensure that we got zero records. + let rep: LookupResponse = from_json(bin).unwrap(); + assert_eq!(rep, LookupResponse { name: vec![] }); + } } ``` @@ -631,22 +658,22 @@ File: `src/bin/schema.rs` Ensure a JSON schema is generated for the smart contract types. ```rust -use cosmwasm_schema::write_api; - -use tutorial::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - write_api! { - instantiate: InstantiateMsg, - execute: ExecuteMsg, - query: QueryMsg, - } +use cosmwasm_schema::write_api; +use name::msg::{ExecuteMsg, InitMsg, QueryMsg}; + +fn main() { + write_api! { + execute: ExecuteMsg, + instantiate: InitMsg, + query: QueryMsg, + } } ``` ## Code Format Before building make sure that everything is formatted correctly using: + ```bash cargo fmt ``` diff --git a/docs/tutorial/07-optimize.md b/docs/tutorial/07-optimize.md index 100ceeed..4fe321d8 100644 --- a/docs/tutorial/07-optimize.md +++ b/docs/tutorial/07-optimize.md @@ -4,15 +4,21 @@ In this section we will optimize the compiled smart contract Wasm to a deployable file. -A rust optimization tool was developed by the CosmWasm team to reduce the size of smart contract Wasm. It is packaged as a docker image. To use this image, add the following to the end of the tutorial `Makefile`. +A rust optimization tool was developed by the CosmWasm team to reduce the size of smart contract Wasm. It is packaged as +a docker image. To use this image, add the following to the end of the tutorial `Makefile`. ```Makefile .PHONY: optimize optimize: - @docker run --rm -v $(CURDIR):/code:Z \ - --mount type=volume,source=tutorial_cache,target=/code/target \ + @if [ "$(UNAME_M)" = "arm64" ]; then \ + image="cosmwasm/optimizer-arm64"; \ + else \ + image="cosmwasm/optimizer"; \ + fi; \ + docker run --rm -v $(CURDIR)/../../:/code:Z --workdir /code/contracts/name \ + --mount type=volume,source=name_cache,target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/rust-optimizer:0.12.10 + $$image:0.16.0 ``` Then build the optimized Wasm @@ -39,7 +45,8 @@ ls -lh target/wasm32-unknown-unknown/release/tutorial.wasm NOTE: Optimized smart contract size must be smaller than `600K` -This concludes Part 1 of the tutorial. The optimized smart contract Wasm is ready to deploy to the Provenance Blockchain. +This concludes Part 1 of the tutorial. The optimized smart contract Wasm is ready to deploy to the Provenance +Blockchain. ## Up Next diff --git a/docs/tutorial/08-setup.md b/docs/tutorial/08-setup.md index 665aed34..33f47598 100644 --- a/docs/tutorial/08-setup.md +++ b/docs/tutorial/08-setup.md @@ -56,6 +56,7 @@ provenanced keys add consumer --home build/run/provenanced --keyring-backend tes ``` Create alias for the keys + ```bash export validator=$(provenanced keys show -a validator --home build/run/provenanced --keyring-backend test -t) export merchant=$(provenanced keys show -a merchant --home build/run/provenanced --keyring-backend test -t) @@ -77,7 +78,6 @@ provenanced tx bank send \ --gas=auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode=block \ --yes \ --testnet \ --output json | jq @@ -97,7 +97,6 @@ provenanced tx bank send \ --gas=auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode=block \ --yes \ --testnet \ --output json | jq @@ -117,7 +116,6 @@ provenanced tx bank send \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -140,7 +138,6 @@ provenanced tx name bind \ --chain-id testing \ --gas-prices="100000nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -160,7 +157,6 @@ provenanced tx marker new 1000000000purchasecoin \ --gas auto \ --gas-prices="1000000nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -180,7 +176,6 @@ provenanced tx marker grant \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -197,7 +192,6 @@ provenanced tx marker finalize purchasecoin \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -214,7 +208,6 @@ provenanced tx marker activate purchasecoin \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -235,7 +228,6 @@ provenanced tx marker withdraw purchasecoin \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq diff --git a/docs/tutorial/09-store.md b/docs/tutorial/09-store.md index 242cd7e9..1011f106 100644 --- a/docs/tutorial/09-store.md +++ b/docs/tutorial/09-store.md @@ -26,7 +26,6 @@ provenanced tx wasm store tutorial.wasm \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq @@ -63,7 +62,8 @@ Should produce output that resembles (field values may differ) the following. } ``` -The `--instantiate-anyof-addresses` flag is important when it is necessary to limit instance creation to specified accounts. +The `--instantiate-anyof-addresses` flag is important when it is necessary to limit instance creation to specified +accounts. Copy the value of the `id` field. It is required to instantiate the contract in the next section. diff --git a/docs/tutorial/10-instantiate.md b/docs/tutorial/10-instantiate.md index 6558ce7e..7afbce86 100644 --- a/docs/tutorial/10-instantiate.md +++ b/docs/tutorial/10-instantiate.md @@ -43,7 +43,6 @@ provenanced tx wasm instantiate 1 \ --gas auto \ --gas-prices="100000nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq diff --git a/docs/tutorial/12-execute.md b/docs/tutorial/12-execute.md index 486dc4c9..9ffb4a9a 100644 --- a/docs/tutorial/12-execute.md +++ b/docs/tutorial/12-execute.md @@ -19,7 +19,6 @@ provenanced tx wasm execute \ --gas auto \ --gas-prices="1905nhash" \ --gas-adjustment=1.5 \ - --broadcast-mode block \ --yes \ --testnet \ --output json | jq diff --git a/docs/tutorial/13-migrate.md b/docs/tutorial/13-migrate.md index 31b8919e..ef644b6d 100644 --- a/docs/tutorial/13-migrate.md +++ b/docs/tutorial/13-migrate.md @@ -39,11 +39,7 @@ File: `src/contract.rs` ```rust /// Called when migrating a contract instance to a new code ID. -pub fn migrate( - deps: DepsMut, - env: Env, - msg: MigrateMsg, -) -> Result { +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { // 1) Ensure the new fee percent is within the updated range. // 2) Get mutable ref to the contract state // 3) Set new fee percent in the state @@ -71,7 +67,7 @@ File: `examples/schema.rs` ```rust use cosmwasm_schema::write_api; -use tutorial::msg::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; +use name::msg::{ExecuteMsg, InitMsg, QueryMsg}; fn main() { write_api! { @@ -81,6 +77,7 @@ fn main() { query: QueryMsg, } } + ``` When complete, use the CLI commands below to migrate the smart contract instance to a new code ID.