diff --git a/Cargo.lock b/Cargo.lock index 7b89de8a8..b76510e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,9 +2158,14 @@ dependencies = [ "dao-dao-macros", "dao-hooks", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", "dao-test-custom-factory", "dao-testing", "dao-voting 2.2.0", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index 39d24543a..1191b5a85 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -11,8 +11,16 @@ version = { workspace = true } 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 = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + [dependencies] cosmwasm-std = { workspace = true } @@ -35,5 +43,10 @@ thiserror = { workspace = true } [dev-dependencies] anyhow = { workspace = true } cw-multi-test = { workspace = true } -dao-testing = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } dao-test-custom-factory = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workpsace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs new file mode 100644 index 000000000..d87edace7 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs @@ -0,0 +1,158 @@ +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, Uint128, WasmMsg}; +use cw721_base::{ + msg::{ + ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, + QueryMsg as Cw721QueryMsg, + }, + MinterResponse, +}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo}, +}; +use dao_testing::test_tube::{cw721_base::Cw721Base, dao_dao_core::DaoCore}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError}; + +use crate::{ + msg::{InstantiateMsg, NftContract, QueryMsg}, + state::Config, + testing::test_tube_env::Cw721VotingContract, +}; + +use super::test_tube_env::{TestEnv, TestEnvBuilder}; + +#[test] +fn test_full_integration_with_factory() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + + // Setup defaults to creating a NFT DAO with the factory contract + // This does not use funds when instantiating the NFT contract. + // We will test that below. + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + .. + } = env.setup(&app); + + // Test instantiating a DAO with a factory contract that requires funds + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721.code_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_binary( + &Cw721ExecuteMsg::::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiating without funds fails + let err = DaoCore::new(&app, &msg, &accounts[0], &[]).unwrap_err(); + + // Error is insufficient funds as no funds were sent + assert_eq!( + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: 0uosmo is smaller than 1000uosmo: insufficient funds".to_string() + }, + err + ); + + // Instantiate DAO succeeds with funds + let dao = DaoCore::new( + &app, + &msg, + &accounts[0], + &[Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + ) + .unwrap(); + + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(&app, vp_contract.code_id, vp_addr.to_string()) + .unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + let cw721_contract = + Cw721Base::new_with_values(&app, cw721.code_id, vp_config.nft_address.to_string()).unwrap(); + + // Check DAO was initialized to minter + let minter: MinterResponse = cw721_contract.query(&Cw721QueryMsg::Minter {}).unwrap(); + assert_eq!(minter.minter, Some(dao.contract_addr.to_string())); +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs index ea43bc797..de0824f52 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -5,6 +5,16 @@ mod instantiate; mod queries; mod tests; +// Integrationg tests using an actual chain binary, requires +// the "test-tube" feature to be enabled +// cargo test --features test-tube +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod integration_tests; +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod test_tube_env; + use cosmwasm_std::Addr; use cw_multi_test::{App, Executor}; use cw_utils::Duration; diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs new file mode 100644 index 000000000..cea9d24a1 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs @@ -0,0 +1,317 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, NftContract, QueryMsg}, + state::Config, +}; + +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, WasmMsg}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo, ProposalModule}, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, threshold::PercentageThreshold, threshold::Threshold, +}; + +use cw721_base::msg::{ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg}; +use dao_testing::test_tube::{ + cw721_base::Cw721Base, dao_dao_core::DaoCore, dao_proposal_single::DaoProposalSingle, + dao_test_custom_factory::CustomFactoryContract, +}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse; +use osmosis_test_tube::{ + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; +pub const JUNO: &str = "ujuno"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub dao: DaoCore<'a>, + pub proposal_single: DaoProposalSingle<'a>, + pub custom_factory: CustomFactoryContract<'a>, + pub vp_contract: Cw721VotingContract<'a>, + pub accounts: Vec, + pub cw721: Cw721Base<'a>, +} + +impl<'a> TestEnv<'a> { + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } +} + +pub struct TestEnvBuilder {} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self {} + } + + // Full DAO setup + pub fn setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + // Upload all needed code ids + let vp_contract_id = Cw721VotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + let cw721_id = Cw721Base::upload(app, &accounts[0]).unwrap(); + + // Instantiate Custom Factory + let custom_factory = CustomFactoryContract::new( + app, + &dao_test_custom_factory::msg::InstantiateMsg {}, + &accounts[0], + ) + .unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_binary( + &Cw721ExecuteMsg::::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + + let cw721 = + Cw721Base::new_with_values(app, cw721_id, vp_config.nft_address.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + TestEnv { + app, + dao, + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + } + } +} + +#[derive(Debug)] +pub struct Cw721VotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> Cw721VotingContract<'a> { + pub fn deploy( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + T: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_cw721_staked.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_cw721_staked-aarch64.wasm"), + ) + .unwrap(), + } + } +} diff --git a/packages/dao-testing/src/test_tube/cw721_base.rs b/packages/dao-testing/src/test_tube/cw721_base.rs new file mode 100644 index 000000000..84c80b8fe --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw721_base.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::{Coin, Empty}; +use cw721_base::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Cw721Base<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> Cw721Base<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw721_base.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw721_base-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index d24469e60..0af2999a4 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -8,6 +8,9 @@ #[cfg(feature = "test-tube")] pub mod cw_tokenfactory_issuer; +#[cfg(feature = "test-tube")] +pub mod cw721_base; + #[cfg(feature = "test-tube")] pub mod dao_dao_core;