diff --git a/cw-orch-daemon/Cargo.toml b/cw-orch-daemon/Cargo.toml index 6900bdf34..c9db4e281 100644 --- a/cw-orch-daemon/Cargo.toml +++ b/cw-orch-daemon/Cargo.toml @@ -17,7 +17,7 @@ exclude = [".env"] all-features = true [features] -default = ["node-tests"] +default = [] # enable node-backed tests (ensure Docker is running) # run with `cargo test --jobs 1 --features node-tests` node-tests = [] diff --git a/cw-orch-daemon/src/builder.rs b/cw-orch-daemon/src/builder.rs index 984de2a9c..1031f4f2d 100644 --- a/cw-orch-daemon/src/builder.rs +++ b/cw-orch-daemon/src/builder.rs @@ -1,4 +1,4 @@ -use crate::{log::print_if_log_disabled, DaemonAsync, DaemonBuilder}; +use crate::{log::print_if_log_disabled, sender::SenderOptions, DaemonAsync, DaemonBuilder}; use std::rc::Rc; use ibc_chain_registry::chain::ChainData; @@ -28,6 +28,8 @@ pub struct DaemonAsyncBuilder { pub(crate) deployment_id: Option, /// Wallet mnemonic pub(crate) mnemonic: Option, + /// Specify Daemon Sender Options + pub(crate) sender_options: SenderOptions, } impl DaemonAsyncBuilder { @@ -53,6 +55,18 @@ impl DaemonAsyncBuilder { self } + /// Specifies whether authz should be used with this daemon + pub fn authz_granter(&mut self, granter: impl ToString) -> &mut Self { + self.sender_options.set_authz_granter(granter); + self + } + + /// Specifies whether a fee grant should be used with this daemon + pub fn fee_granter(&mut self, granter: impl ToString) -> &mut Self { + self.sender_options.set_fee_granter(granter); + self + } + /// Build a daemon pub async fn build(&self) -> Result { let chain = self @@ -65,10 +79,11 @@ impl DaemonAsyncBuilder { .unwrap_or(DEFAULT_DEPLOYMENT.to_string()); let state = Rc::new(DaemonState::new(chain, deployment_id, false).await?); // if mnemonic provided, use it. Else use env variables to retrieve mnemonic + let sender_options = self.sender_options.clone(); let sender = if let Some(mnemonic) = &self.mnemonic { - Sender::from_mnemonic(&state, mnemonic)? + Sender::from_mnemonic_with_options(&state, mnemonic, sender_options)? } else { - Sender::new(&state)? + Sender::new_with_options(&state, sender_options)? }; let daemon = DaemonAsync { state, @@ -85,6 +100,7 @@ impl From for DaemonAsyncBuilder { chain: value.chain, deployment_id: value.deployment_id, mnemonic: value.mnemonic, + sender_options: value.sender_options, } } } diff --git a/cw-orch-daemon/src/core.rs b/cw-orch-daemon/src/core.rs index 9951e129e..0b958abdd 100644 --- a/cw-orch-daemon/src/core.rs +++ b/cw-orch-daemon/src/core.rs @@ -106,7 +106,7 @@ impl DaemonAsync { contract_address: &Addr, ) -> Result { let exec_msg: MsgExecuteContract = MsgExecuteContract { - sender: self.sender.pub_addr()?, + sender: self.sender.msg_sender()?, contract: AccountId::from_str(contract_address.as_str())?, msg: serde_json::to_vec(&exec_msg)?, funds: parse_cw_coins(coins)?, @@ -132,7 +132,7 @@ impl DaemonAsync { code_id, label: Some(label.unwrap_or("instantiate_contract").to_string()), admin: admin.map(|a| FromStr::from_str(a.as_str()).unwrap()), - sender: sender.pub_addr()?, + sender: self.sender.msg_sender()?, msg: serde_json::to_vec(&init_msg)?, funds: parse_cw_coins(coins)?, }; @@ -169,7 +169,7 @@ impl DaemonAsync { contract_address: &Addr, ) -> Result { let exec_msg: MsgMigrateContract = MsgMigrateContract { - sender: self.sender.pub_addr()?, + sender: self.sender.msg_sender()?, contract: AccountId::from_str(contract_address.as_str())?, msg: serde_json::to_vec(&migrate_msg)?, code_id: new_code_id, @@ -243,7 +243,7 @@ impl DaemonAsync { e.write_all(&file_contents)?; let wasm_byte_code = e.finish()?; let store_msg = cosmrs::cosmwasm::MsgStoreCode { - sender: sender.pub_addr()?, + sender: self.sender.msg_sender()?, wasm_byte_code, instantiate_permission: None, }; diff --git a/cw-orch-daemon/src/lib.rs b/cw-orch-daemon/src/lib.rs index 8c0209998..54d080894 100644 --- a/cw-orch-daemon/src/lib.rs +++ b/cw-orch-daemon/src/lib.rs @@ -31,6 +31,7 @@ pub(crate) mod cosmos_modules { pub use cosmrs::proto::{ cosmos::{ auth::v1beta1 as auth, + authz::v1beta1 as authz, bank::v1beta1 as bank, base::{abci::v1beta1 as abci, tendermint::v1beta1 as tendermint}, feegrant::v1beta1 as feegrant, diff --git a/cw-orch-daemon/src/log.rs b/cw-orch-daemon/src/log.rs index 9bbf5f942..cceb990b5 100644 --- a/cw-orch-daemon/src/log.rs +++ b/cw-orch-daemon/src/log.rs @@ -8,7 +8,7 @@ static LOGS_DISABLED: Once = Once::new(); // Prints a warning if log is disabled for the application pub fn print_if_log_disabled() -> Result<(), DaemonError> { - LOGS_DISABLED.call_once(|| { + LOGS_DISABLED.call_once(|| { // Here we check for logging capabilities. if !log::log_enabled!(log::Level::Info) && !CwOrchEnvVars::load().map(|env|env.disable_logs_message).unwrap_or(false){ println!( diff --git a/cw-orch-daemon/src/queriers.rs b/cw-orch-daemon/src/queriers.rs index 67e3e5e99..df6be4f99 100644 --- a/cw-orch-daemon/src/queriers.rs +++ b/cw-orch-daemon/src/queriers.rs @@ -44,6 +44,7 @@ macro_rules! cosmos_query { }; } +mod authz; mod bank; mod cosmwasm; mod feegrant; @@ -52,6 +53,7 @@ mod ibc; mod node; mod staking; +pub use authz::Authz; pub use bank::{cosmrs_to_cosmwasm_coins, Bank}; pub use cosmwasm::CosmWasm; pub use feegrant::Feegrant; diff --git a/cw-orch-daemon/src/queriers/authz.rs b/cw-orch-daemon/src/queriers/authz.rs new file mode 100644 index 000000000..9af47937c --- /dev/null +++ b/cw-orch-daemon/src/queriers/authz.rs @@ -0,0 +1,79 @@ +use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; +use tonic::transport::Channel; + +use crate::{cosmos_modules, error::DaemonError}; + +use super::DaemonQuerier; + +/// Queries for Cosmos AuthZ Module +pub struct Authz { + channel: Channel, +} + +impl DaemonQuerier for Authz { + fn new(channel: Channel) -> Self { + Self { channel } + } +} + +impl Authz { + /// Query Authz Grants from grantee to granter + pub async fn grants( + &self, + granter: String, + grantee: String, + msg_type_url: String, + pagination: Option, + ) -> Result { + use cosmos_modules::authz::{query_client::QueryClient, QueryGrantsRequest}; + let mut client: QueryClient = QueryClient::new(self.channel.clone()); + let grants = client + .grants(QueryGrantsRequest { + granter, + grantee, + msg_type_url, + pagination, + }) + .await? + .into_inner(); + Ok(grants) + } + + /// Query Authz Grants of grantee + pub async fn grantee_grants( + &self, + grantee: String, + pagination: Option, + ) -> Result + { + use cosmos_modules::authz::{query_client::QueryClient, QueryGranteeGrantsRequest}; + let mut client: QueryClient = QueryClient::new(self.channel.clone()); + let grants = client + .grantee_grants(QueryGranteeGrantsRequest { + grantee, + pagination, + }) + .await? + .into_inner(); + Ok(grants) + } + + /// Query Authz Grants for granter + pub async fn granter_grants( + &self, + granter: String, + pagination: Option, + ) -> Result + { + use cosmos_modules::authz::{query_client::QueryClient, QueryGranterGrantsRequest}; + let mut client: QueryClient = QueryClient::new(self.channel.clone()); + let grants = client + .granter_grants(QueryGranterGrantsRequest { + granter, + pagination, + }) + .await? + .into_inner(); + Ok(grants) + } +} diff --git a/cw-orch-daemon/src/sender.rs b/cw-orch-daemon/src/sender.rs index c35107a28..037950168 100644 --- a/cw-orch-daemon/src/sender.rs +++ b/cw-orch-daemon/src/sender.rs @@ -25,7 +25,7 @@ use crate::{core::parse_cw_coins, keys::private::PrivateKey}; use cosmrs::{ bank::MsgSend, crypto::secp256k1::SigningKey, - proto::traits::Message, + proto::{cosmos::authz::v1beta1::MsgExec, traits::Message}, tendermint::chain::Id, tx::{self, ModeInfo, Msg, Raw, SignDoc, SignMode, SignerInfo}, AccountId, Any, @@ -48,14 +48,47 @@ pub type Wallet = Rc>; /// Signer of the transactions and helper for address derivation /// This is the main interface for simulating and signing transactions +#[derive(Clone)] pub struct Sender { pub private_key: PrivateKey, pub secp: Secp256k1, pub(crate) daemon_state: Rc, + pub(crate) options: SenderOptions, +} + +#[derive(Default, Clone)] +#[non_exhaustive] +pub struct SenderOptions { + pub authz_granter: Option, + pub fee_granter: Option, +} + +impl SenderOptions { + pub fn authz_granter(mut self, granter: impl ToString) -> Self { + self.authz_granter = Some(granter.to_string()); + self + } + pub fn fee_granter(mut self, granter: impl ToString) -> Self { + self.fee_granter = Some(granter.to_string()); + self + } + pub fn set_authz_granter(&mut self, granter: impl ToString) { + self.authz_granter = Some(granter.to_string()); + } + pub fn set_fee_granter(&mut self, granter: impl ToString) { + self.fee_granter = Some(granter.to_string()); + } } impl Sender { pub fn new(daemon_state: &Rc) -> Result, DaemonError> { + Self::new_with_options(daemon_state, SenderOptions::default()) + } + + pub fn new_with_options( + daemon_state: &Rc, + options: SenderOptions, + ) -> Result, DaemonError> { let kind = ChainKind::from(daemon_state.chain_data.network_type.clone()); // NETWORK_MNEMONIC_GROUP let env_variable_name = kind.mnemonic_env_variable_name(); @@ -66,13 +99,22 @@ impl Sender { ) }); - Self::from_mnemonic(daemon_state, &mnemonic) + Self::from_mnemonic_with_options(daemon_state, &mnemonic, options) } - /// Construct a new Sender from a mnemonic + /// Construct a new Sender from a mnemonic with additional options pub fn from_mnemonic( daemon_state: &Rc, mnemonic: &str, + ) -> Result, DaemonError> { + Self::from_mnemonic_with_options(daemon_state, mnemonic, SenderOptions::default()) + } + + /// Construct a new Sender from a mnemonic with additional options + pub fn from_mnemonic_with_options( + daemon_state: &Rc, + mnemonic: &str, + options: SenderOptions, ) -> Result, DaemonError> { let secp = Secp256k1::new(); let p_key: PrivateKey = @@ -82,6 +124,7 @@ impl Sender { daemon_state: daemon_state.clone(), private_key: p_key, secp, + options, }; log::info!( target: &local_target(), @@ -92,6 +135,14 @@ impl Sender { Ok(sender) } + pub fn authz_granter(&mut self, granter: impl Into) { + self.options.authz_granter = Some(granter.into()); + } + + pub fn fee_granter(&mut self, granter: impl Into) { + self.options.fee_granter = Some(granter.into()); + } + fn cosmos_private_key(&self) -> SigningKey { SigningKey::from_slice(&self.private_key.raw_key()).unwrap() } @@ -115,13 +166,24 @@ impl Sender { Ok(self.pub_addr()?.to_string()) } + /// Returns the actual sender of every message sent. + /// If an authz granter is set, returns the authz granter + /// Else, returns the address associated with the current private key + pub fn msg_sender(&self) -> Result { + if let Some(sender) = &self.options.authz_granter { + Ok(sender.parse()?) + } else { + self.pub_addr() + } + } + pub async fn bank_send( &self, recipient: &str, coins: Vec, ) -> Result { let msg_send = MsgSend { - from_address: self.pub_addr()?, + from_address: self.msg_sender()?, to_address: AccountId::from_str(recipient)?, amount: parse_cw_coins(&coins)?, }; @@ -169,6 +231,7 @@ impl Sender { 0u8, &self.daemon_state.chain_data.fees.fee_tokens[0].denom, 0, + self.options.clone(), )?; let auth_info = SignerInfo { @@ -239,6 +302,20 @@ impl Sender { ) -> Result { let timeout_height = Node::new(self.channel()).block_height().await? + 10u64; + let msgs = if self.options.authz_granter.is_some() { + // We wrap authz messages + vec![Any { + type_url: "/cosmos.authz.v1beta1.MsgExec".to_string(), + value: MsgExec { + grantee: self.pub_addr_str()?, + msgs, + } + .encode_to_vec(), + }] + } else { + msgs + }; + let tx_body = TxBuilder::build_body(msgs, memo, timeout_height); let tx_builder = TxBuilder::new(tx_body); diff --git a/cw-orch-daemon/src/sync/builder.rs b/cw-orch-daemon/src/sync/builder.rs index 2f7e87486..d150de025 100644 --- a/cw-orch-daemon/src/sync/builder.rs +++ b/cw-orch-daemon/src/sync/builder.rs @@ -1,6 +1,6 @@ use ibc_chain_registry::chain::ChainData; -use crate::DaemonAsyncBuilder; +use crate::{sender::SenderOptions, DaemonAsyncBuilder}; use super::{super::error::DaemonError, core::Daemon}; @@ -24,6 +24,8 @@ pub struct DaemonBuilder { pub(crate) deployment_id: Option, /// Wallet mnemonic pub(crate) mnemonic: Option, + /// Specify Daemon Sender Options + pub(crate) sender_options: SenderOptions, } impl DaemonBuilder { @@ -64,6 +66,18 @@ impl DaemonBuilder { self } + /// Specifies wether authz should be used with this daemon + pub fn authz_granter(&mut self, granter: impl ToString) -> &mut Self { + self.sender_options.set_authz_granter(granter.to_string()); + self + } + + /// Specifies wether authz should be used with this daemon + pub fn fee_granter(&mut self, granter: impl ToString) -> &mut Self { + self.sender_options.set_fee_granter(granter.to_string()); + self + } + /// Build a Daemon pub fn build(&self) -> Result { let rt_handle = self diff --git a/cw-orch-daemon/src/sync/core.rs b/cw-orch-daemon/src/sync/core.rs index 1d416d194..1866bd867 100644 --- a/cw-orch-daemon/src/sync/core.rs +++ b/cw-orch-daemon/src/sync/core.rs @@ -3,6 +3,7 @@ use std::{fmt::Debug, rc::Rc, time::Duration}; use super::super::{sender::Wallet, DaemonAsync}; use crate::{ queriers::{cosmrs_to_cosmwasm_coins, Bank, DaemonQuerier, Node}, + sender::SenderOptions, CosmTxResponse, DaemonBuilder, DaemonError, DaemonState, }; @@ -71,6 +72,33 @@ impl Daemon { pub fn wallet(&self) -> Wallet { self.daemon.sender.clone() } + + /// Adds authz capability to the returned Daemon + pub fn with_authz_granter(&self, granter: impl ToString) -> Self { + let mut new_daemon = self.clone(); + let mut new_sender = (*self.daemon.sender).clone(); + new_sender.authz_granter(granter.to_string()); + new_daemon.daemon.sender = Rc::new(new_sender); + new_daemon + } + + /// Adds authz capability to the returned Daemon + pub fn with_fee_granter(&self, granter: impl ToString) -> Self { + let mut new_daemon = self.clone(); + let mut new_sender = (*self.daemon.sender).clone(); + new_sender.fee_granter(granter.to_string()); + new_daemon.daemon.sender = Rc::new(new_sender); + new_daemon + } + + /// Modifies all the sender options in one go + pub fn with_sender_options(&self, options: SenderOptions) -> Self { + let mut new_daemon = self.clone(); + let mut new_sender = (*self.daemon.sender).clone(); + new_sender.options = options; + new_daemon.daemon.sender = Rc::new(new_sender); + new_daemon + } } impl ChainState for Daemon { diff --git a/cw-orch-daemon/src/tx_builder.rs b/cw-orch-daemon/src/tx_builder.rs index e281660ec..db107b81d 100644 --- a/cw-orch-daemon/src/tx_builder.rs +++ b/cw-orch-daemon/src/tx_builder.rs @@ -10,7 +10,8 @@ use cosmrs::{ Any, Coin, }; use cw_orch_core::log::transaction_target; -use cw_orch_core::CwOrchEnvVars; + +use crate::sender::SenderOptions; use super::{sender::Sender, DaemonError}; @@ -65,10 +66,11 @@ impl TxBuilder { amount: impl Into, denom: &str, gas_limit: u64, + sender_options: SenderOptions, ) -> Result { let fee = Coin::new(amount.into(), denom).unwrap(); let mut fee = Fee::from_amount_and_gas(fee, gas_limit); - fee.granter = CwOrchEnvVars::load()? + fee.granter = sender_options .fee_granter .map(|g| AccountId::from_str(&g)) .transpose()?; @@ -132,7 +134,12 @@ impl TxBuilder { (fee_amount, gas_expected) }; - let fee = Self::build_fee(tx_fee, &wallet.get_fee_token(), gas_limit)?; + let fee = Self::build_fee( + tx_fee, + &wallet.get_fee_token(), + gas_limit, + wallet.options.clone(), + )?; log::debug!( target: &transaction_target(), diff --git a/cw-orch-daemon/tests/authz.rs b/cw-orch-daemon/tests/authz.rs new file mode 100644 index 000000000..1222dc5fe --- /dev/null +++ b/cw-orch-daemon/tests/authz.rs @@ -0,0 +1,148 @@ +mod common; +#[cfg(feature = "node-tests")] +mod tests { + /* + Authz tests + */ + + use cosmrs::proto::cosmos::{ + authz::v1beta1::{ + GenericAuthorization, GrantAuthorization, MsgGrant, MsgGrantResponse, + QueryGranteeGrantsResponse, QueryGranterGrantsResponse, QueryGrantsResponse, + }, + bank::v1beta1::MsgSend, + }; + use cosmwasm_std::coins; + use cw_orch_core::environment::{BankQuerier, TxHandler}; + use cw_orch_daemon::{queriers::Authz, Daemon}; + use cw_orch_networks::networks::LOCAL_JUNO; + use cw_orch_traits::Stargate; + use prost::Message; + use prost::Name; + use prost_types::{Any, Timestamp}; + + pub const SECOND_MNEMONIC: &str ="salute trigger antenna west ignore own dance bounce battle soul girl scan test enroll luggage sorry distance traffic brand keen rich syrup wood repair"; + + #[test] + #[serial_test::serial] + fn authz() -> anyhow::Result<()> { + use cw_orch_networks::networks; + + let runtime = tokio::runtime::Runtime::new().unwrap(); + + let daemon = Daemon::builder() + .chain(networks::LOCAL_JUNO) + .handle(runtime.handle()) + .build() + .unwrap(); + + let sender = daemon.sender().to_string(); + + let second_daemon = Daemon::builder() + .chain(networks::LOCAL_JUNO) + .handle(runtime.handle()) + .authz_granter(sender.clone()) + .mnemonic(SECOND_MNEMONIC) + .build() + .unwrap(); + + let grantee = second_daemon.sender().to_string(); + + let current_timestamp = daemon.block_info()?.time; + + let authorization = cosmrs::Any { + type_url: "/cosmos.authz.v1beta1.GenericAuthorization".to_string(), + value: GenericAuthorization { + msg: MsgSend::type_url(), + } + .encode_to_vec(), + }; + let expiration = Timestamp { + seconds: (current_timestamp.seconds() + 3600) as i64, + nanos: 0, + }; + let grant = cosmrs::proto::cosmos::authz::v1beta1::Grant { + authorization: Some(authorization.clone()), + expiration: Some(expiration.clone()), + }; + // We start by granting authz to an account + daemon.commit_any::( + vec![Any { + type_url: "/cosmos.authz.v1beta1.MsgGrant".to_string(), + value: MsgGrant { + granter: sender.clone(), + grantee: grantee.clone(), + grant: Some(grant.clone()), + } + .encode_to_vec(), + }], + None, + )?; + + // Check Queries of the authz + let grant_authorization = GrantAuthorization { + granter: sender.clone(), + grantee: grantee.clone(), + authorization: Some(authorization.clone()), + expiration: Some(expiration.clone()), + }; + + // Grants + let authz_querier: Authz = daemon.query_client(); + let grants: QueryGrantsResponse = runtime.handle().block_on(async { + authz_querier + .grants(sender.clone(), grantee.clone(), MsgSend::type_url(), None) + .await + })?; + assert_eq!(grants.grants, vec![grant]); + + // Grantee grants + let grantee_grants: QueryGranteeGrantsResponse = runtime + .handle() + .block_on(async { authz_querier.grantee_grants(grantee.clone(), None).await })?; + assert_eq!(grantee_grants.grants, vec![grant_authorization.clone()]); + + // Granter grants + let granter_grants: QueryGranterGrantsResponse = runtime + .handle() + .block_on(async { authz_querier.granter_grants(sender.clone(), None).await })?; + assert_eq!(granter_grants.grants, vec![grant_authorization]); + + // No grant gives out an error + runtime + .handle() + .block_on(async { + authz_querier + .grants(grantee.clone(), sender.clone(), MsgSend::type_url(), None) + .await + }) + .unwrap_err(); + + // Check use of grants + + // The we send some funds to the account + runtime.block_on( + daemon + .daemon + .sender + .bank_send(&grantee, coins(1_000_000, LOCAL_JUNO.gas_denom)), + )?; + + // And send a large amount of tokens on their behalf + runtime.block_on( + second_daemon + .daemon + .sender + .bank_send(&grantee, coins(5_000_000, LOCAL_JUNO.gas_denom)), + )?; + + // the balance of the grantee whould be 6_000_000 or close + + let grantee_balance = + daemon.balance(grantee.clone(), Some(LOCAL_JUNO.gas_denom.to_string()))?; + + assert_eq!(grantee_balance.first().unwrap().amount.u128(), 6_000_000); + + Ok(()) + } +} diff --git a/packages/cw-orch-core/src/env.rs b/packages/cw-orch-core/src/env.rs index 0cb0f0d7b..d23021644 100644 --- a/packages/cw-orch-core/src/env.rs +++ b/packages/cw-orch-core/src/env.rs @@ -25,7 +25,6 @@ pub const DISABLE_WALLET_BALANCE_ASSERTION_ENV_NAME: &str = "CW_ORCH_DISABLE_WALLET_BALANCE_ASSERTION"; pub const DISABLE_MANUAL_INTERACTION_ENV_NAME: &str = "CW_ORCH_DISABLE_MANUAL_INTERACTION"; pub const DISABLE_ENABLE_LOGS_MESSAGE_ENV_NAME: &str = "CW_ORCH_DISABLE_ENABLE_LOGS_MESSAGE"; -pub const FEE_GRANTER_ENV_NAME: &str = "CW_ORCH_FEE_GRANTER"; pub const MAIN_MNEMONIC_ENV_NAME: &str = "MAIN_MNEMONIC"; pub const TEST_MNEMONIC_ENV_NAME: &str = "TEST_MNEMONIC"; pub const LOCAL_MNEMONIC_ENV_NAME: &str = "LOCAL_MNEMONIC"; @@ -101,10 +100,6 @@ pub struct CwOrchEnvVars { /// Disable the "Enable Logs" message /// It allows forcing cw-orch to not output anything pub disable_logs_message: bool, - /// Optional - string - /// Specify a fee granter for interacting with a chain - /// This allows interacting with the blockchain and make another address pay for your fees (if allowed) - pub fee_granter: Option, } /// Fetches the default state folder. @@ -136,7 +131,6 @@ impl Default for CwOrchEnvVars { disable_wallet_balance_assertion: false, disable_manual_interaction: false, disable_logs_message: false, - fee_granter: None, } } } @@ -185,9 +179,6 @@ impl CwOrchEnvVars { if let Ok(str_value) = env::var(LOCAL_MNEMONIC_ENV_NAME) { env_values.local_mnemonic = Some(str_value); } - if let Ok(str_value) = env::var(FEE_GRANTER_ENV_NAME) { - env_values.fee_granter = Some(str_value); - } Ok(env_values) } } diff --git a/packages/cw-orch-core/src/environment/cosmwasm_environment.rs b/packages/cw-orch-core/src/environment/cosmwasm_environment.rs index 8de7deecb..71de4fa92 100644 --- a/packages/cw-orch-core/src/environment/cosmwasm_environment.rs +++ b/packages/cw-orch-core/src/environment/cosmwasm_environment.rs @@ -123,3 +123,107 @@ pub trait BankQuerier: TxHandler { /// Query total supply in the bank for a denom fn supply_of(&self, denom: impl Into) -> Result::Error>; } + +// TODO: Perfect test candidate for `trybuild` +#[cfg(test)] +mod tests { + use cw_multi_test::AppResponse; + + use super::*; + + #[derive(Clone)] + struct MockHandler {} + + impl ChainState for MockHandler { + type Out = (); + + fn state(&self) -> Self::Out {} + } + + impl TxHandler for MockHandler { + type Response = AppResponse; + + type Error = CwEnvError; + + type ContractSource = (); + + type Sender = (); + + fn sender(&self) -> Addr { + unimplemented!() + } + + fn set_sender(&mut self, _sender: Self::Sender) {} + + fn wait_blocks(&self, _amount: u64) -> Result<(), Self::Error> { + Ok(()) + } + + fn wait_seconds(&self, _secs: u64) -> Result<(), Self::Error> { + Ok(()) + } + + fn next_block(&self) -> Result<(), Self::Error> { + Ok(()) + } + + fn block_info(&self) -> Result { + unimplemented!() + } + + fn upload( + &self, + _contract_source: &impl Uploadable, + ) -> Result { + unimplemented!() + } + + fn instantiate( + &self, + _code_id: u64, + _init_msg: &I, + _label: Option<&str>, + _admin: Option<&Addr>, + _coins: &[cosmwasm_std::Coin], + ) -> Result { + unimplemented!() + } + + fn execute( + &self, + _exec_msg: &E, + _coins: &[Coin], + _contract_address: &Addr, + ) -> Result { + unimplemented!() + } + + fn query( + &self, + _query_msg: &Q, + _contract_address: &Addr, + ) -> Result { + unimplemented!() + } + + fn migrate( + &self, + _migrate_msg: &M, + _new_code_id: u64, + _contract_address: &Addr, + ) -> Result { + unimplemented!() + } + } + + fn associated_error(t: T) -> anyhow::Result<()> { + t.wait_blocks(5)?; + Ok(()) + } + + #[test] + fn tx_handler_error_usable_on_anyhow() -> anyhow::Result<()> { + associated_error(MockHandler {})?; + Ok(()) + } +}