From 889959c894262f47c62864527faeba583b06a7c6 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 17 Sep 2024 12:45:15 +0000 Subject: [PATCH 01/10] Added cw-plus interface --- Cargo.toml | 1 + packages/integrations/cw-plus/Cargo.toml | 40 ++ .../integrations/cw-plus/src/cw1_subkeys.rs | 122 +++++ .../integrations/cw-plus/src/cw1_whitelist.rs | 81 ++++ .../integrations/cw-plus/src/cw20_base.rs | 179 +++++++ .../integrations/cw-plus/src/cw20_ics20.rs | 98 ++++ .../cw-plus/src/cw3_fixed_multisig.rs | 99 ++++ .../cw-plus/src/cw3_flex_multisig.rs | 105 +++++ .../integrations/cw-plus/src/cw4_group.rs | 78 ++++ .../integrations/cw-plus/src/cw4_stake.rs | 93 ++++ packages/integrations/cw-plus/src/lib.rs | 12 + .../cw-plus/tests/interface_tests.rs | 440 ++++++++++++++++++ .../macros/from-interface-derive/Cargo.toml | 16 + .../macros/from-interface-derive/src/lib.rs | 54 +++ 14 files changed, 1418 insertions(+) create mode 100644 packages/integrations/cw-plus/Cargo.toml create mode 100644 packages/integrations/cw-plus/src/cw1_subkeys.rs create mode 100644 packages/integrations/cw-plus/src/cw1_whitelist.rs create mode 100644 packages/integrations/cw-plus/src/cw20_base.rs create mode 100644 packages/integrations/cw-plus/src/cw20_ics20.rs create mode 100644 packages/integrations/cw-plus/src/cw3_fixed_multisig.rs create mode 100644 packages/integrations/cw-plus/src/cw3_flex_multisig.rs create mode 100644 packages/integrations/cw-plus/src/cw4_group.rs create mode 100644 packages/integrations/cw-plus/src/cw4_stake.rs create mode 100644 packages/integrations/cw-plus/src/lib.rs create mode 100644 packages/integrations/cw-plus/tests/interface_tests.rs create mode 100644 packages/macros/from-interface-derive/Cargo.toml create mode 100644 packages/macros/from-interface-derive/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index d78a9da48..091f75fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "test_contracts/*", "packages/macros/*", "packages/interchain/*", + "packages/integrations/*", ] exclude = [ "test_contracts/compatibility-test", # TODO: add new after cw-orch-core 2.0.0 as it's breaking, it shouldn't be compatible diff --git a/packages/integrations/cw-plus/Cargo.toml b/packages/integrations/cw-plus/Cargo.toml new file mode 100644 index 000000000..e3ebda378 --- /dev/null +++ b/packages/integrations/cw-plus/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "cw-plus-orch" +version = "0.25.0" # Version of cw-orch that's used +edition = "2021" +description = "CW4 implementation of group based on staked tokens" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-plus" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] + +[dependencies] +cosmwasm-std = { workspace = true } +cw-utils = { workspace = true } +cosmwasm-schema = { version = "2.0.0" } + + +cw-controllers = { version = "2.0.0" } +cw1 = { version = "2.0.0" } +cw1-subkeys = { version = "2.0.0", features = ["library"] } +cw1-whitelist = { version = "2.0.0", features = ["library"] } +cw3 = { version = "2.0.0" } +cw3-fixed-multisig = { version = "2.0.0", features = ["library"] } +cw3-flex-multisig = { version = "2.0.0", features = ["library"] } +cw4 = { version = "2.0.0" } +cw4-group = { version = "2.0.0", features = ["library"] } +cw4-stake = { version = "2.0.0", features = ["library"] } +cw20 = { version = "2.0.0" } +cw20-base = { version = "2.0.0", features = ["library"] } +cw20-ics20 = { version = "2.0.0", features = ["library"] } + +cw-orch-from-interface-derive = { version = "0.1.0", path = "../../macros/from-interface-derive" } + +cw-orch = { workspace = true } + +[dev-dependencies] +cw-orch-interchain = { workspace = true } diff --git a/packages/integrations/cw-plus/src/cw1_subkeys.rs b/packages/integrations/cw-plus/src/cw1_subkeys.rs new file mode 100644 index 000000000..c73214c7c --- /dev/null +++ b/packages/integrations/cw-plus/src/cw1_subkeys.rs @@ -0,0 +1,122 @@ +use cw_orch::interface; + +use cw1_subkeys::contract; +pub use cw1_subkeys::msg::{ExecuteMsg, QueryMsg}; +pub use cw1_whitelist::msg::InstantiateMsg; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw1SubKeys; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw1SubKeys { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw1_subkeys") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + ) + .with_migrate(contract::migrate), + ) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + use cosmwasm_schema::schemars::JsonSchema; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + enum ExecuteMsgInterface + where + T: Clone + std::fmt::Debug + PartialEq + JsonSchema, + { + /// Execute requests the contract to re-dispatch all these messages with the + /// contract's address as sender. Every implementation has it's own logic to + /// determine in + // This method is renamed to not conflict with [`CwOrchExecute::execute`] + #[cw_orch(fn_name("execute_requests"))] + Execute { + msgs: Vec>, + }, + /// Freeze will make a mutable contract immutable, must be called by an admin + Freeze {}, + /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, + /// and only works if the contract is mutable + UpdateAdmins { admins: Vec }, + + /// Add an allowance to a given subkey (subkey must not be admin) + IncreaseAllowance { + spender: String, + amount: Coin, + expires: Option, + }, + /// Decreases an allowance for a given subkey (subkey must not be admin) + DecreaseAllowance { + spender: String, + amount: Coin, + expires: Option, + }, + + // Setups up permissions for a given subkey. + SetPermissions { + spender: String, + permissions: cw1_subkeys::state::Permissions, + }, + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + enum QueryMsgInterface + where + T: Clone + std::fmt::Debug + PartialEq + JsonSchema, + { + /// Shows all admins and whether or not it is mutable + #[returns(cw1_whitelist::msg::AdminListResponse)] + AdminList {}, + /// Get the current allowance for the given subkey (how much it can spend) + #[returns(cw1_subkeys::state::Allowance)] + Allowance { spender: String }, + /// Get the current permissions for the given subkey (how much it can spend) + #[returns(cw1_subkeys::msg::PermissionsInfo)] + Permissions { spender: String }, + /// Checks permissions of the caller on this proxy. + /// If CanExecute returns true then a call to `Execute` with the same message, + /// before any further state changes, should also succeed. + #[returns(cw1::CanExecuteResponse)] + CanExecute { + sender: String, + msg: cosmwasm_std::CosmosMsg, + }, + /// Gets all Allowances for this contract + #[returns(cw1_subkeys::msg::AllAllowancesResponse)] + AllAllowances { + start_after: Option, + limit: Option, + }, + /// Gets all Permissions for this contract + #[returns(cw1_subkeys::msg::AllPermissionsResponse)] + AllPermissions { + start_after: Option, + limit: Option, + }, + } +} diff --git a/packages/integrations/cw-plus/src/cw1_whitelist.rs b/packages/integrations/cw-plus/src/cw1_whitelist.rs new file mode 100644 index 000000000..daa5fea35 --- /dev/null +++ b/packages/integrations/cw-plus/src/cw1_whitelist.rs @@ -0,0 +1,81 @@ +use cw_orch::interface; + +use cw1_whitelist::contract; +pub use cw1_whitelist::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw1Whitelist; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw1Whitelist { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw1_whitelist") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + )) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + use cosmwasm_schema::schemars::JsonSchema; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + enum ExecuteMsgInterface + where + T: Clone + std::fmt::Debug + PartialEq + JsonSchema, + { + /// Execute requests the contract to re-dispatch all these messages with the + /// contract's address as sender. Every implementation has it's own logic to + /// determine in + // This method is renamed to not conflict with [`CwOrchExecute::execute`] + #[cw_orch(fn_name("execute_requests"))] + Execute { + msgs: Vec>, + }, + /// Freeze will make a mutable contract immutable, must be called by an admin + Freeze {}, + /// UpdateAdmins will change the admin set of the contract, must be called by an existing admin, + /// and only works if the contract is mutable + UpdateAdmins { admins: Vec }, + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + enum QueryMsgInterface + where + T: Clone + std::fmt::Debug + PartialEq + JsonSchema, + { + /// Shows all admins and whether or not it is mutable + #[returns(cw1_whitelist::msg::AdminListResponse)] + AdminList {}, + /// Checks permissions of the caller on this proxy. + /// If CanExecute returns true then a call to `Execute` with the same message, + /// before any further state changes, should also succeed. + #[returns(cw1::CanExecuteResponse)] + CanExecute { + sender: String, + msg: cosmwasm_std::CosmosMsg, + }, + } +} diff --git a/packages/integrations/cw-plus/src/cw20_base.rs b/packages/integrations/cw-plus/src/cw20_base.rs new file mode 100644 index 000000000..19b233ebc --- /dev/null +++ b/packages/integrations/cw-plus/src/cw20_base.rs @@ -0,0 +1,179 @@ +use cw_orch::interface; + +use cw20_base::contract; +pub use cw20_base::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +// TODO: cw20 Migrate doesn't implement Debug: https://github.com/CosmWasm/cw-plus/pull/910 +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, cosmwasm_std::Empty)] +pub struct Cw20Base; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +use crate::{WASM_RELEASE_TAG, WASM_REPO_NAME, WASM_REPO_OWNER}; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw20Base { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + WasmPath::github_release( + WASM_REPO_OWNER, + WASM_REPO_NAME, + WASM_RELEASE_TAG, + "cw20_base.wasm", + ) + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + ) + .with_migrate(contract::migrate), + ) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + + pub enum ExecuteMsgInterface { + /// Transfer is a base message to move tokens to another account without triggering actions + Transfer { + recipient: String, + amount: cosmwasm_std::Uint128, + }, + /// Burn is a base message to destroy tokens forever + Burn { amount: cosmwasm_std::Uint128 }, + /// Send is a base message to transfer tokens to a contract and trigger an action + /// on the receiving contract. + Send { + contract: String, + amount: cosmwasm_std::Uint128, + msg: cosmwasm_std::Binary, + }, + /// Only with "approval" extension. Allows spender to access an additional amount tokens + /// from the owner's (env.sender) account. If expires is Some(), overwrites current allowance + /// expiration with this one. + IncreaseAllowance { + spender: String, + amount: cosmwasm_std::Uint128, + expires: Option, + }, + /// Only with "approval" extension. Lowers the spender's access of tokens + /// from the owner's (env.sender) account by amount. If expires is Some(), overwrites current + /// allowance expiration with this one. + DecreaseAllowance { + spender: String, + amount: cosmwasm_std::Uint128, + expires: Option, + }, + /// Only with "approval" extension. Transfers amount tokens from owner -> recipient + /// if `env.sender` has sufficient pre-approval. + TransferFrom { + owner: String, + recipient: String, + amount: cosmwasm_std::Uint128, + }, + /// Only with "approval" extension. Sends amount tokens from owner -> contract + /// if `env.sender` has sufficient pre-approval. + SendFrom { + owner: String, + contract: String, + amount: cosmwasm_std::Uint128, + msg: cosmwasm_std::Binary, + }, + /// Only with "approval" extension. Destroys tokens forever + BurnFrom { + owner: String, + amount: cosmwasm_std::Uint128, + }, + /// Only with the "mintable" extension. If authorized, creates amount new tokens + /// and adds to the recipient balance. + Mint { + recipient: String, + amount: cosmwasm_std::Uint128, + }, + /// Only with the "mintable" extension. The current minter may set + /// a new minter. Setting the minter to None will remove the + /// token's minter forever. + UpdateMinter { new_minter: Option }, + /// Only with the "marketing" extension. If authorized, updates marketing metadata. + /// Setting None/null for any of these will leave it unchanged. + /// Setting Some("") will clear this field on the contract storage + UpdateMarketing { + /// A URL pointing to the project behind this token. + project: Option, + /// A longer description of the token and it's utility. Designed for tooltips or such + description: Option, + /// The address (if any) who can update this data structure + marketing: Option, + }, + /// If set as the "marketing" role on the contract, upload a new URL, SVG, or PNG for the token + UploadLogo(cw20::Logo), + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + pub enum QueryMsgInterface { + /// Returns the current balance of the given address, 0 if unset. + #[returns(cw20::BalanceResponse)] + Balance { address: String }, + /// Returns metadata on the contract - name, decimals, supply, etc. + #[returns(cw20::TokenInfoResponse)] + TokenInfo {}, + /// Only with "mintable" extension. + /// Returns who can mint and the hard cap on maximum tokens after minting. + #[returns(cw20::MinterResponse)] + Minter {}, + /// Only with "allowance" extension. + /// Returns how much spender can use from owner account, 0 if unset. + #[returns(cw20::AllowanceResponse)] + Allowance { owner: String, spender: String }, + /// Only with "enumerable" extension (and "allowances") + /// Returns all allowances this owner has approved. Supports pagination. + #[returns(cw20::AllAllowancesResponse)] + AllAllowances { + owner: String, + start_after: Option, + limit: Option, + }, + /// Only with "enumerable" extension (and "allowances") + /// Returns all allowances this spender has been granted. Supports pagination. + #[returns(cw20::AllSpenderAllowancesResponse)] + AllSpenderAllowances { + spender: String, + start_after: Option, + limit: Option, + }, + /// Only with "enumerable" extension + /// Returns all accounts that have balances. Supports pagination. + #[returns(cw20::AllAccountsResponse)] + AllAccounts { + start_after: Option, + limit: Option, + }, + /// Only with "marketing" extension + /// Returns more metadata on the contract to display in the client: + /// - description, logo, project url, etc. + #[returns(cw20::MarketingInfoResponse)] + MarketingInfo {}, + /// Only with "marketing" extension + /// Downloads the embedded logo data (if stored on chain). Errors if no logo data is stored for this + /// contract. + #[returns(cw20::DownloadLogoResponse)] + DownloadLogo {}, + } +} diff --git a/packages/integrations/cw-plus/src/cw20_ics20.rs b/packages/integrations/cw-plus/src/cw20_ics20.rs new file mode 100644 index 000000000..c7cc1b217 --- /dev/null +++ b/packages/integrations/cw-plus/src/cw20_ics20.rs @@ -0,0 +1,98 @@ +use cw20_ics20::{ + contract, + ibc::{ + ibc_channel_close, ibc_channel_connect, ibc_channel_open, ibc_packet_ack, + ibc_packet_receive, ibc_packet_timeout, + }, +}; +use cw_orch::interface; + +pub use cw20_ics20::msg::{AllowMsg, ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, TransferMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InitMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct Cw20Ics20; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw20Ics20 { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw20_ics20") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + ) + .with_migrate(contract::migrate) + .with_ibc( + ibc_channel_open, + ibc_channel_connect, + ibc_channel_close, + ibc_packet_receive, + ibc_packet_ack, + ibc_packet_timeout, + ), + ) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + pub enum ExecuteMsgInterface { + /// This accepts a properly-encoded ReceiveMsg from a cw20 contract + Receive(cw20::Cw20ReceiveMsg), + /// This allows us to transfer *exactly one* native token + #[cw_orch(payable)] + Transfer(cw20_ics20::msg::TransferMsg), + /// This must be called by gov_contract, will allow a new cw20 token to be sent + Allow(cw20_ics20::msg::AllowMsg), + /// Change the admin (must be called by current admin) + UpdateAdmin { admin: String }, + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + pub enum QueryMsgInterface { + /// Return the port ID bound by this contract. + #[returns(cw20_ics20::msg::PortResponse)] + Port {}, + /// Show all channels we have connected to. + #[returns(cw20_ics20::msg::ListChannelsResponse)] + ListChannels {}, + /// Returns the details of the name channel, error if not created. + #[returns(cw20_ics20::msg::ChannelResponse)] + Channel { id: String }, + /// Show the Config. + #[returns(cw20_ics20::msg::ConfigResponse)] + Config {}, + #[returns(cw_controllers::AdminResponse)] + Admin {}, + /// Query if a given cw20 contract is allowed. + #[returns(cw20_ics20::msg::AllowedResponse)] + Allowed { contract: String }, + /// List all allowed cw20 contracts. + #[returns(cw20_ics20::msg::ListAllowedResponse)] + ListAllowed { + start_after: Option, + limit: Option, + }, + } +} diff --git a/packages/integrations/cw-plus/src/cw3_fixed_multisig.rs b/packages/integrations/cw-plus/src/cw3_fixed_multisig.rs new file mode 100644 index 000000000..2c2c5d62c --- /dev/null +++ b/packages/integrations/cw-plus/src/cw3_fixed_multisig.rs @@ -0,0 +1,99 @@ +use cw_orch::interface; + +use cw3_fixed_multisig::contract; +pub use cw3_fixed_multisig::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw3FixedMultisig; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw3FixedMultisig { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw3_fixed_multisig") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + )) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + enum ExecuteMsgInterface { + Propose { + title: String, + description: String, + msgs: Vec>, + // note: we ignore API-spec'd earliest if passed, always opens immediately + latest: Option, + }, + Vote { + proposal_id: u64, + vote: cw3::Vote, + }, + // This method is renamed to not conflict with [`CwOrchExecute::execute`] + #[cw_orch(fn_name("execute_proposal"))] + Execute { + proposal_id: u64, + }, + Close { + proposal_id: u64, + }, + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + enum QueryMsgInterface { + #[returns(cw_utils::ThresholdResponse)] + Threshold {}, + #[returns(cw3::ProposalResponse)] + Proposal { proposal_id: u64 }, + #[returns(cw3::ProposalListResponse)] + ListProposals { + start_after: Option, + limit: Option, + }, + #[returns(cw3::ProposalListResponse)] + ReverseProposals { + start_before: Option, + limit: Option, + }, + #[returns(cw3::VoteResponse)] + // This method is renamed to not conflict with [`ExecuteMsgInterface::Vote`] + #[cw_orch(fn_name("get_vote"))] + Vote { proposal_id: u64, voter: String }, + #[returns(cw3::VoteListResponse)] + ListVotes { + proposal_id: u64, + start_after: Option, + limit: Option, + }, + #[returns(cw3::VoterResponse)] + Voter { address: String }, + #[returns(cw3::VoterListResponse)] + ListVoters { + start_after: Option, + limit: Option, + }, + } +} diff --git a/packages/integrations/cw-plus/src/cw3_flex_multisig.rs b/packages/integrations/cw-plus/src/cw3_flex_multisig.rs new file mode 100644 index 000000000..6b4963b33 --- /dev/null +++ b/packages/integrations/cw-plus/src/cw3_flex_multisig.rs @@ -0,0 +1,105 @@ +use cw_orch::interface; + +use cw3_flex_multisig::contract; +pub use cw3_flex_multisig::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw3FlexMultisig; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw3FlexMultisig { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw3_flex_multisig") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + )) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + enum ExecuteMsgInterface { + #[cw_orch(payable)] + Propose { + title: String, + description: String, + msgs: Vec>, + // note: we ignore API-spec'd earliest if passed, always opens immediately + latest: Option, + }, + Vote { + proposal_id: u64, + vote: cw3::Vote, + }, + // This method is renamed to not conflict with [`CwOrchExecute::execute`] + #[cw_orch(fn_name("execute_proposal"))] + Execute { + proposal_id: u64, + }, + Close { + proposal_id: u64, + }, + /// Handles update hook messages from the group contract + MemberChangedHook(cw4::MemberChangedHookMsg), + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + pub enum QueryMsgInterface { + #[returns(cw_utils::ThresholdResponse)] + Threshold {}, + #[returns(cw3::ProposalResponse)] + Proposal { proposal_id: u64 }, + #[returns(cw3::ProposalListResponse)] + ListProposals { + start_after: Option, + limit: Option, + }, + #[returns(cw3::ProposalListResponse)] + ReverseProposals { + start_before: Option, + limit: Option, + }, + #[returns(cw3::VoteResponse)] + // This method is renamed to not conflict with [`ExecuteMsgInterface::Vote`] + #[cw_orch(fn_name("get_vote"))] + Vote { proposal_id: u64, voter: String }, + #[returns(cw3::VoteListResponse)] + ListVotes { + proposal_id: u64, + start_after: Option, + limit: Option, + }, + #[returns(cw3::VoterResponse)] + Voter { address: String }, + #[returns(cw3::VoterListResponse)] + ListVoters { + start_after: Option, + limit: Option, + }, + /// Gets the current configuration. + #[returns(cw3_flex_multisig::state::Config)] + Config {}, + } +} diff --git a/packages/integrations/cw-plus/src/cw4_group.rs b/packages/integrations/cw-plus/src/cw4_group.rs new file mode 100644 index 000000000..5871d919b --- /dev/null +++ b/packages/integrations/cw-plus/src/cw4_group.rs @@ -0,0 +1,78 @@ +use cw_orch::interface; + +use cw4_group::contract; +pub use cw4_group::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw4Group; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw4Group { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw4_group") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + )) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + pub enum ExecuteMsgInterface { + /// Change the admin + UpdateAdmin { admin: Option }, + /// apply a diff to the existing members. + /// remove is applied after add, so if an address is in both, it is removed + UpdateMembers { + remove: Vec, + add: Vec, + }, + /// Add a new hook to be informed of all membership changes. Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + pub enum QueryMsgInterface { + #[returns(cw_controllers::AdminResponse)] + Admin {}, + #[returns(cw4::TotalWeightResponse)] + TotalWeight { at_height: Option }, + #[returns(cw4::MemberListResponse)] + ListMembers { + start_after: Option, + limit: Option, + }, + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, + } +} diff --git a/packages/integrations/cw-plus/src/cw4_stake.rs b/packages/integrations/cw-plus/src/cw4_stake.rs new file mode 100644 index 000000000..32b84d173 --- /dev/null +++ b/packages/integrations/cw-plus/src/cw4_stake.rs @@ -0,0 +1,93 @@ +use cw_orch::interface; + +use cw4_stake::contract; +pub use cw4_stake::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +#[cfg(not(target_arch = "wasm32"))] +pub use interfaces::{AsyncQueryMsgInterfaceFns, ExecuteMsgInterfaceFns, QueryMsgInterfaceFns}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct Cw4Stake; + +#[cfg(not(target_arch = "wasm32"))] +use cw_orch::prelude::*; + +#[cfg(not(target_arch = "wasm32"))] +impl Uploadable for Cw4Stake { + // Return the path to the wasm file + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw1_whitelist") + .unwrap() + } + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty( + contract::execute, + contract::instantiate, + contract::query, + )) + } +} + +#[cfg(not(target_arch = "wasm32"))] +/// Copy messages of the contract to implement cw-orch helpers on Execute([`cw_orch::ExecuteFns`]) and Query([`cw_orch::QueryFns`]) interfaces +mod interfaces { + use super::*; + + #[derive(cw_orch::ExecuteFns, cw_orch_from_interface_derive::FromInterface)] + pub enum ExecuteMsgInterface { + /// Bond will bond all staking tokens sent with the message and update membership weight + #[cw_orch(payable)] + Bond {}, + /// Unbond will start the unbonding process for the given number of tokens. + /// The sender immediately loses weight from these tokens, and can claim them + /// back to his wallet after `unbonding_period` + Unbond { tokens: cosmwasm_std::Uint128 }, + /// Claim is used to claim your native tokens that you previously "unbonded" + /// after the contract-defined waiting period (eg. 1 week) + Claim {}, + + /// Change the admin + UpdateAdmin { admin: Option }, + /// Add a new hook to be informed of all membership changes. Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + + /// This accepts a properly-encoded ReceiveMsg from a cw20 contract + Receive(cw20::Cw20ReceiveMsg), + } + + #[cosmwasm_schema::cw_serde] + #[derive( + cosmwasm_schema::QueryResponses, + cw_orch::QueryFns, + cw_orch_from_interface_derive::FromInterface, + )] + pub enum QueryMsgInterface { + /// Claims shows the tokens in process of unbonding for this address + #[returns(cw_controllers::ClaimsResponse)] + Claims { address: String }, + // Show the number of tokens currently staked by this address. + #[returns(cw4_stake::msg::StakedResponse)] + Staked { address: String }, + + #[returns(cw_controllers::AdminResponse)] + Admin {}, + #[returns(cw4::TotalWeightResponse)] + TotalWeight {}, + #[returns(cw4::MemberListResponse)] + ListMembers { + start_after: Option, + limit: Option, + }, + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, + } +} diff --git a/packages/integrations/cw-plus/src/lib.rs b/packages/integrations/cw-plus/src/lib.rs new file mode 100644 index 000000000..765fcad6e --- /dev/null +++ b/packages/integrations/cw-plus/src/lib.rs @@ -0,0 +1,12 @@ +pub mod cw1_subkeys; +pub mod cw1_whitelist; +pub mod cw20_base; +pub mod cw20_ics20; +pub mod cw3_fixed_multisig; +pub mod cw3_flex_multisig; +pub mod cw4_group; +pub mod cw4_stake; + +pub const WASM_RELEASE_TAG: &str = "v1.1.2"; +pub const WASM_REPO_OWNER: &str = "cosmwasm"; +pub const WASM_REPO_NAME: &str = "cw-plus"; diff --git a/packages/integrations/cw-plus/tests/interface_tests.rs b/packages/integrations/cw-plus/tests/interface_tests.rs new file mode 100644 index 000000000..786a1ac5a --- /dev/null +++ b/packages/integrations/cw-plus/tests/interface_tests.rs @@ -0,0 +1,440 @@ +mod cw1_subkeys { + use cw1_whitelist::msg::AdminListResponse; + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::cw1_subkeys::{ + Cw1SubKeys, ExecuteMsgInterfaceFns, InstantiateMsg, QueryMsgInterfaceFns, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + let contract = Cw1SubKeys::new("cw1", chain.clone()); + contract.upload().unwrap(); + contract + .instantiate( + &InstantiateMsg { + admins: vec![chain.sender_addr().to_string()], + mutable: true, + }, + None, + &[], + ) + .unwrap(); + contract.execute_requests(vec![]).unwrap(); + + let admins = contract.admin_list().unwrap(); + assert_eq!( + admins, + AdminListResponse { + admins: vec![chain.sender_addr().to_string()], + mutable: true + } + ); + contract.freeze().unwrap(); + let admins = contract.admin_list().unwrap(); + assert_eq!( + admins, + AdminListResponse { + admins: vec![chain.sender_addr().to_string()], + mutable: false + } + ) + } +} + +mod cw1_whitelist { + use cw1_whitelist::msg::AdminListResponse; + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::cw1_whitelist::{ + Cw1Whitelist, ExecuteMsgInterfaceFns, InstantiateMsg, QueryMsgInterfaceFns, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + let contract = Cw1Whitelist::new("cw1", chain.clone()); + contract.upload().unwrap(); + contract + .instantiate( + &InstantiateMsg { + admins: vec![chain.sender_addr().to_string()], + mutable: true, + }, + None, + &[], + ) + .unwrap(); + contract.execute_requests(vec![]).unwrap(); + + let admins = contract.admin_list().unwrap(); + assert_eq!( + admins, + AdminListResponse { + admins: vec![chain.sender_addr().to_string()], + mutable: true + } + ); + contract.freeze().unwrap(); + let admins = contract.admin_list().unwrap(); + assert_eq!( + admins, + AdminListResponse { + admins: vec![chain.sender_addr().to_string()], + mutable: false + } + ) + } +} + +mod cw3_fixed_multisig { + use cw3_fixed_multisig::msg::Voter; + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::cw3_fixed_multisig::{ + Cw3FixedMultisig, ExecuteMsgInterfaceFns, InstantiateMsg, QueryMsgInterfaceFns, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + let voter = chain.addr_make("voter"); + let contract = Cw3FixedMultisig::new("cw3", chain.clone()); + contract.upload().unwrap(); + contract + .instantiate( + &InstantiateMsg { + voters: vec![ + Voter { + addr: voter.to_string(), + weight: 1, + }, + Voter { + addr: chain.sender_addr().to_string(), + weight: 1, + }, + ], + threshold: cw_utils::Threshold::AbsoluteCount { weight: 2 }, + max_voting_period: cw_utils::Duration::Time(42424242), + }, + None, + &[], + ) + .unwrap(); + contract + .call_as(&voter) + .propose("foobar", vec![], "title", None) + .unwrap(); + let proposals = contract.list_proposals(None, None).unwrap(); + let proposal_id = proposals.proposals[0].id; + contract.vote(proposal_id, cw3::Vote::Yes).unwrap(); + contract.execute_proposal(proposal_id).unwrap(); + + let proposal = contract.proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, cw3::Status::Executed); + } +} + +mod cw4_group_cw3_flex_multisig { + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::{ + cw3_flex_multisig::{ + Cw3FlexMultisig, ExecuteMsgInterfaceFns as _, InstantiateMsg as Cw3InstantiateMsg, + QueryMsgInterfaceFns as _, + }, + cw4_group::{ + Cw4Group, ExecuteMsgInterfaceFns, InstantiateMsg as Cw4InstantiateMsg, + QueryMsgInterfaceFns as _, + }, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + let voter1 = chain.addr_make("voter1"); + let voter2 = chain.addr_make("voter2"); + let cw4 = Cw4Group::new("cw4", chain.clone()); + cw4.upload().unwrap(); + cw4.instantiate( + &Cw4InstantiateMsg { + admin: Some(chain.sender_addr().to_string()), + members: vec![ + cw4::Member { + addr: voter1.to_string(), + weight: 1, + }, + cw4::Member { + addr: voter2.to_string(), + weight: 1, + }, + ], + }, + None, + &[], + ) + .unwrap(); + chain.wait_blocks(10).unwrap(); + + let hook_addr = chain.addr_make("hook"); + cw4.add_hook(hook_addr.to_string()).unwrap(); + let hooks = cw4.hooks().unwrap(); + assert_eq!(hooks.hooks, vec![hook_addr.to_string()]); + cw4.remove_hook(hook_addr.to_string()).unwrap(); + let hooks = cw4.hooks().unwrap(); + assert!(hooks.hooks.is_empty()); + + let cw3 = Cw3FlexMultisig::new("cw3", chain.clone()); + cw3.upload().unwrap(); + cw3.instantiate( + &Cw3InstantiateMsg { + group_addr: cw4.address().unwrap().to_string(), + threshold: cw_utils::Threshold::AbsoluteCount { weight: 2 }, + max_voting_period: cw_utils::Duration::Time(1111111), + executor: Some(cw3_flex_multisig::state::Executor::Only( + chain.sender_addr(), + )), + proposal_deposit: None, + }, + None, + &[], + ) + .unwrap(); + cw3.call_as(&voter1) + .propose("foobar", vec![], "title", None, &[]) + .unwrap(); + let proposals = cw3.list_proposals(None, None).unwrap(); + let proposal_id = proposals.proposals[0].id; + cw3.call_as(&voter2) + .vote(proposal_id, cw3::Vote::Yes) + .unwrap(); + cw3.execute_proposal(proposal_id).unwrap(); + + let proposal = cw3.proposal(proposal_id).unwrap(); + assert_eq!(proposal.status, cw3::Status::Executed); + } +} + +mod cw4_stake { + use cosmwasm_std::{coins, Uint128}; + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::cw4_stake::{ + Cw4Stake, ExecuteMsgInterfaceFns, InstantiateMsg, QueryMsgInterfaceFns, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + + let cw4 = Cw4Stake::new("cw4", chain.clone()); + cw4.upload().unwrap(); + cw4.instantiate( + &InstantiateMsg { + denom: cw20::Denom::Native("abc".to_owned()), + tokens_per_weight: Uint128::one(), + min_bond: Uint128::one(), + unbonding_period: cw_utils::Duration::Time(1231230000), + admin: None, + }, + None, + &[], + ) + .unwrap(); + + let user1 = chain.addr_make("one"); + let user2 = chain.addr_make("two"); + + chain.add_balance(&user1, coins(100, "abc")).unwrap(); + chain.add_balance(&user2, coins(300, "abc")).unwrap(); + cw4.call_as(&user1).bond(&coins(100, "abc")).unwrap(); + cw4.call_as(&user2).bond(&coins(300, "abc")).unwrap(); + + let members = cw4.list_members(None, None).unwrap().members; + assert!(members.contains(&cw4::Member { + addr: user1.to_string(), + weight: 100 + })); + assert!(members.contains(&cw4::Member { + addr: user2.to_string(), + weight: 300 + })); + } +} + +mod cw20_base { + use cosmwasm_std::Uint128; + use cw20::MinterResponse; + use cw_orch::{mock::Mock, prelude::*}; + use cw_plus_orch::cw20_base::{ + Cw20Base, ExecuteMsgInterfaceFns, InstantiateMsg, QueryMsgInterfaceFns, + }; + + #[test] + fn check_interface() { + let chain = Mock::new("sender"); + + let cw20 = Cw20Base::new("cw20", chain.clone()); + cw20.upload().unwrap(); + cw20.instantiate( + &InstantiateMsg { + name: "foobar".to_owned(), + symbol: "foo".to_owned(), + decimals: 12, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: chain.sender_addr().to_string(), + cap: None, + }), + marketing: None, + }, + Some(&chain.sender_addr()), + &[], + ) + .unwrap(); + + let user1 = chain.addr_make("one"); + let user2 = chain.addr_make("two"); + + // Mint 100 for user1 + cw20.mint(100u128, user1.to_string()).unwrap(); + // User 1 shares 10 with user2 + cw20.call_as(&user1) + .transfer(10u128, user2.to_string()) + .unwrap(); + + // Check new balance of user1 + let balance = cw20.balance(user1.to_string()).unwrap().balance; + assert_eq!(balance, Uint128::new(90)); + + // Check that user2 registered + let accounts = cw20.all_accounts(None, None).unwrap().accounts; + assert!(accounts.contains(&user2.to_string())); + + // Can "migrate" with empty + cw20.migrate(&Empty {}, cw20.code_id().unwrap()).unwrap(); + } +} + +mod cw20_ics { + use cosmwasm_std::{coins, to_json_binary, Uint128}; + use cw20::MinterResponse; + use cw20_base::msg::InstantiateMsg; + use cw20_ics20::{ + ibc::{ICS20_ORDERING, ICS20_VERSION}, + msg::AllowedInfo, + }; + use cw_orch::prelude::*; + use cw_orch_interchain::{env::contract_port, prelude::*}; + use cw_plus_orch::{ + cw20_base::{Cw20Base, ExecuteMsgInterfaceFns as _}, + cw20_ics20::{ + AllowMsg, Cw20Ics20, ExecuteMsgInterfaceFns, InitMsg, QueryMsgInterfaceFns, TransferMsg, + }, + }; + + #[test] + fn check_interface() { + let interchain = + MockBech32InterchainEnv::new(vec![("juno-1", "sender"), ("stargaze-1", "sender")]); + + let juno = interchain.get_chain("juno-1").unwrap(); + let stargaze = interchain.get_chain("stargaze-1").unwrap(); + + let gov_juno = juno.addr_make("gov"); + + let cw20 = Cw20Base::new("cw20", juno.clone()); + cw20.upload().unwrap(); + cw20.instantiate( + &InstantiateMsg { + name: "foobar".to_owned(), + symbol: "foo".to_owned(), + decimals: 12, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: juno.sender_addr().to_string(), + cap: None, + }), + marketing: None, + }, + None, + &[], + ) + .unwrap(); + let cw20_ics20 = Cw20Ics20::new("cw20_ics20", juno.clone()); + cw20_ics20.upload().unwrap(); + cw20_ics20 + .instantiate( + &InitMsg { + default_timeout: 3600, + gov_contract: gov_juno.to_string(), + allowlist: vec![AllowMsg { + contract: cw20.addr_str().unwrap(), + gas_limit: None, + }], + default_gas_limit: None, + }, + None, + &[], + ) + .unwrap(); + let allow_list = cw20_ics20.list_allowed(None, None).unwrap().allow; + assert_eq!( + allow_list, + vec![AllowedInfo { + contract: cw20.addr_str().unwrap(), + gas_limit: None + }] + ); + + let channel = interchain + .create_channel( + "juno-1", + "stargaze-1", + &contract_port(&cw20_ics20), + &PortId::transfer(), + ICS20_VERSION, + Some(ICS20_ORDERING), + ) + .unwrap(); + let channel = channel + .interchain_channel + .get_ordered_ports_from("juno-1") + .unwrap(); + let channels = cw20_ics20.list_channels().unwrap().channels; + let expected_channel_id = channel.0.channel.unwrap().to_string(); + assert_eq!(channels[0].id, expected_channel_id); + + let user_juno = juno.addr_make("juno"); + let user_stargaze = stargaze.addr_make("stargaze"); + + let transfer_msg = TransferMsg { + channel: expected_channel_id, + remote_address: user_stargaze.to_string(), + timeout: None, + memo: None, + }; + // Send 100 cw20 coins to stargaze + cw20.mint(100_u128, user_juno.to_string()).unwrap(); + let response = cw20 + .call_as(&user_juno) + .send( + 100_u128, + cw20_ics20.addr_str().unwrap(), + to_json_binary(&transfer_msg).unwrap(), + ) + .unwrap(); + interchain + .await_and_check_packets("juno-1", response) + .unwrap(); + // Send 200 native coins to stargaze + juno.add_balance(&user_juno, coins(200, "denom")).unwrap(); + let response = cw20_ics20 + .call_as(&user_juno) + .transfer(transfer_msg, &coins(200, "denom")) + .unwrap(); + interchain + .await_and_check_packets("juno-1", response) + .unwrap(); + + let balance = stargaze.balance(&user_stargaze, None).unwrap(); + assert_eq!(balance[0].amount, Uint128::new(100)); + assert_eq!(balance[1].amount, Uint128::new(200)); + } +} diff --git a/packages/macros/from-interface-derive/Cargo.toml b/packages/macros/from-interface-derive/Cargo.toml new file mode 100644 index 000000000..aa39dca66 --- /dev/null +++ b/packages/macros/from-interface-derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cw-orch-from-interface-derive" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Macro for generating From implementation for cw-orch interfaces of copied types" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +quote = "1" +proc-macro2 = "1" +syn = { version = "2", features = ["full", "extra-traits", "visit-mut"] } diff --git a/packages/macros/from-interface-derive/src/lib.rs b/packages/macros/from-interface-derive/src/lib.rs new file mode 100644 index 000000000..60c37ca2c --- /dev/null +++ b/packages/macros/from-interface-derive/src/lib.rs @@ -0,0 +1,54 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemEnum}; +const INTERFACE_POSTFIX: &str = "Interface"; + +#[proc_macro_derive(FromInterface)] +pub fn from_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as ItemEnum); + let (impl_generics, ty_generics, where_clause) = &ast.generics.split_for_impl(); + + let interface_name = &ast.ident; + let original_name = { + let counter_name = interface_name.to_string(); + // Supporting "{OriginalName}Interface" format only, to simplify macro and interface names + let (counter_name, _) = counter_name + .split_once(INTERFACE_POSTFIX) + .expect(r#"Interface message type supposed to have "Interface" postfix"#); + proc_macro2::Ident::new(counter_name, proc_macro2::Span::call_site()) + }; + let froms = ast.variants.into_iter().map(|variant| { + let variant_name = variant.ident.clone(); + let fields = match variant.fields { + syn::Fields::Unnamed(variant_fields) => { + let variant_fields = (0..variant_fields.unnamed.len()).map(|i| { + proc_macro2::Ident::new(&format!("arg{i}"), proc_macro2::Span::call_site()) + }); + quote!( ( #(#variant_fields,)* ) ) + } + syn::Fields::Named(variant_fields) => { + let idents = variant_fields + .named + .into_iter() + .map(|field| field.ident.unwrap()); + quote!( { #(#idents,)* } ) + } + syn::Fields::Unit => quote!(), + }; + quote! ( #interface_name::#variant_name #fields => #original_name::#variant_name #fields ) + }); + quote!( + impl #impl_generics From<#interface_name #ty_generics> for #original_name #ty_generics + #where_clause + { + fn from(value: #interface_name #ty_generics) -> #original_name #ty_generics { + match value { + #(#froms,)* + } + } + } + ) + .into() +} From de3740e07a17263fabe3b2efea4314863bdb9ad8 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 17 Sep 2024 13:57:13 +0000 Subject: [PATCH 02/10] Download wasms exmaple script --- .gitignore | 4 +- packages/integrations/cw-plus/Cargo.toml | 6 +++ .../cw-plus/examples/download-wasms.rs | 50 +++++++++++++++++++ .../cw-plus/examples/test-wasm.rs | 28 +++++++++++ .../integrations/cw-plus/src/cw20_base.rs | 11 ++-- 5 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 packages/integrations/cw-plus/examples/download-wasms.rs create mode 100644 packages/integrations/cw-plus/examples/test-wasm.rs diff --git a/.gitignore b/.gitignore index a0c840f2c..ce12d33f1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ gen_cov_test **/logs/ -docs/book \ No newline at end of file +docs/book + +packages/integrations/**/artifacts \ No newline at end of file diff --git a/packages/integrations/cw-plus/Cargo.toml b/packages/integrations/cw-plus/Cargo.toml index e3ebda378..9de8b643d 100644 --- a/packages/integrations/cw-plus/Cargo.toml +++ b/packages/integrations/cw-plus/Cargo.toml @@ -38,3 +38,9 @@ cw-orch = { workspace = true } [dev-dependencies] cw-orch-interchain = { workspace = true } +cw-orch = { workspace = true, features = ["daemon"] } +dotenv = "0.15.0" +octocrab = "0.39.0" +pretty_env_logger = "0.5.0" +reqwest = "0.12.7" +tokio.workspace = true diff --git a/packages/integrations/cw-plus/examples/download-wasms.rs b/packages/integrations/cw-plus/examples/download-wasms.rs new file mode 100644 index 000000000..89f82d73b --- /dev/null +++ b/packages/integrations/cw-plus/examples/download-wasms.rs @@ -0,0 +1,50 @@ +use cw_orch::anyhow; +use std::{io::Cursor, path::PathBuf, str::FromStr}; + +pub const CW_PLUS_REPO_OWNER: &str = "CosmWasm"; +pub const CW_PLUS_REPO_NAME: &str = "cw-plus"; +pub const CW_PLUS_RELEASE_TAG: &str = "v2.0.0"; + +pub const ALL_CONTRACTS: &[&str] = &[ + "cw1_subkeys", + "cw1_whitelist", + "cw3_fixed_multisig", + "cw3_flex_multisig", + "cw4_group", + "cw4_stake", + "cw20_base", + "cw20_ics20", +]; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let crate_dir = env!("CARGO_MANIFEST_DIR"); + let artifacts_dir = PathBuf::from_str(crate_dir)?.join("artifacts"); + + // We create the artifacts directory if non-existent + std::fs::create_dir_all(&artifacts_dir)?; + + // We get the release, common for all artifacts + let release = octocrab::instance() + .repos(CW_PLUS_REPO_OWNER, CW_PLUS_REPO_NAME) + .releases() + .get_by_tag(CW_PLUS_RELEASE_TAG) + .await?; + + for contract in ALL_CONTRACTS { + let release_file_name = format!("{contract}.wasm"); + let file_name = artifacts_dir.join(&release_file_name); + + let wasm_asset = release + .assets + .iter() + .find(|asset| asset.name.eq(&release_file_name)) + .unwrap(); + + let response = reqwest::get(wasm_asset.browser_download_url.clone()).await?; + let mut file = std::fs::File::create(file_name)?; + let mut content = Cursor::new(response.bytes().await?); + std::io::copy(&mut content, &mut file)?; + } + Ok(()) +} diff --git a/packages/integrations/cw-plus/examples/test-wasm.rs b/packages/integrations/cw-plus/examples/test-wasm.rs new file mode 100644 index 000000000..9aec40acd --- /dev/null +++ b/packages/integrations/cw-plus/examples/test-wasm.rs @@ -0,0 +1,28 @@ +use cw_orch::daemon::networks::LOCAL_JUNO; +use cw_orch::prelude::*; +use cw_plus_orch::cw1_subkeys::Cw1SubKeys; +use cw_plus_orch::cw1_whitelist::Cw1Whitelist; +use cw_plus_orch::cw20_base::Cw20Base; +use cw_plus_orch::cw20_ics20::Cw20Ics20; +use cw_plus_orch::cw3_fixed_multisig::Cw3FixedMultisig; +use cw_plus_orch::cw3_flex_multisig::Cw3FlexMultisig; +use cw_plus_orch::cw4_group::Cw4Group; +use cw_plus_orch::cw4_stake::Cw4Stake; + +fn main() -> cw_orch::anyhow::Result<()> { + dotenv::dotenv()?; + pretty_env_logger::init(); + + let daemon = Daemon::builder(LOCAL_JUNO).build()?; + + Cw1SubKeys::new("cw1_subkeys", daemon.clone()).upload()?; + Cw1Whitelist::new("cw1_whitelist", daemon.clone()).upload()?; + Cw3FixedMultisig::new("cw3_fixed_multisig", daemon.clone()).upload()?; + Cw3FlexMultisig::new("cw3_flex_multisig", daemon.clone()).upload()?; + Cw4Group::new("cw4_group", daemon.clone()).upload()?; + Cw4Stake::new("cw4_stake", daemon.clone()).upload()?; + Cw20Base::new("cw20_base", daemon.clone()).upload()?; + Cw20Ics20::new("cw20_ics20", daemon.clone()).upload()?; + + Ok(()) +} diff --git a/packages/integrations/cw-plus/src/cw20_base.rs b/packages/integrations/cw-plus/src/cw20_base.rs index 19b233ebc..1eb901e6b 100644 --- a/packages/integrations/cw-plus/src/cw20_base.rs +++ b/packages/integrations/cw-plus/src/cw20_base.rs @@ -12,18 +12,13 @@ pub struct Cw20Base; #[cfg(not(target_arch = "wasm32"))] use cw_orch::prelude::*; -use crate::{WASM_RELEASE_TAG, WASM_REPO_NAME, WASM_REPO_OWNER}; - #[cfg(not(target_arch = "wasm32"))] impl Uploadable for Cw20Base { // Return the path to the wasm file fn wasm(_chain: &ChainInfoOwned) -> WasmPath { - WasmPath::github_release( - WASM_REPO_OWNER, - WASM_REPO_NAME, - WASM_RELEASE_TAG, - "cw20_base.wasm", - ) + artifacts_dir_from_workspace!() + .find_wasm_path("cw20_base") + .unwrap() } // Return a CosmWasm contract wrapper fn wrapper() -> Box> { From 0ab67be45c387770f10923ac4ca78faa3c9a7efb Mon Sep 17 00:00:00 2001 From: Kayanski Date: Tue, 17 Sep 2024 14:01:37 +0000 Subject: [PATCH 03/10] Amended publish scripts --- before_publish.sh | 1 + publish.sh | 32 +++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100755 before_publish.sh diff --git a/before_publish.sh b/before_publish.sh new file mode 100755 index 000000000..a761b2991 --- /dev/null +++ b/before_publish.sh @@ -0,0 +1 @@ +cargo run --example download-wasms -p cw-plus-orch \ No newline at end of file diff --git a/publish.sh b/publish.sh index 7b6670788..ee9f0a730 100755 --- a/publish.sh +++ b/publish.sh @@ -1,5 +1,7 @@ #!/bin/bash +# Before publishing, we need to download the cw-plus artifacts +./before_publish.sh # Before publishing, test the version with the Abstract implementation to make sure you're not breaking important API set -o errexit -o nounset -o pipefail @@ -25,7 +27,19 @@ BASE_PACKAGES=" cw-orch-mock cw-orch-networks " -CORE="cw-orch-daemon cw-orch" + +INTERCHAIN_PACKAGES=" + interchain-core + starship + interchain-daemon + interchain-mock +" + +CORE="cw-orch-daemon cw-orch cw-orch-interchain" + +INTEGRATIONS=" + cw-plus +" for pack in $BASE_PACKAGES; do ( @@ -35,6 +49,14 @@ for pack in $BASE_PACKAGES; do ) done +for lib in $INTERCHAIN; do + ( + cd "packages/interchain/$lib" + echo "Publishing $lib" + cargo publish + ) +done + for lib in $CORE; do ( cd "$lib" @@ -43,6 +65,14 @@ for lib in $CORE; do ) done +for integration in $INTEGRATIONS; do + ( + cd "packages/integrations/$integration" + echo "Publishing $integration" + cargo publish + ) +done + echo "Everything is published!" # From a076890448e66e853a596d507ba9842190e7c2e7 Mon Sep 17 00:00:00 2001 From: Kayanski <44806566+Kayanski@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:14 +0200 Subject: [PATCH 04/10] Update packages/integrations/cw-plus/Cargo.toml Co-authored-by: Mykhailo Donchenko <91957742+Buckram123@users.noreply.github.com> --- packages/integrations/cw-plus/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/integrations/cw-plus/Cargo.toml b/packages/integrations/cw-plus/Cargo.toml index 9de8b643d..c3a5a4d1e 100644 --- a/packages/integrations/cw-plus/Cargo.toml +++ b/packages/integrations/cw-plus/Cargo.toml @@ -10,8 +10,6 @@ documentation = "https://docs.cosmwasm.com" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] - [dependencies] cosmwasm-std = { workspace = true } cw-utils = { workspace = true } From 2948f9c423d17f3eb4ab5b4ab9430fa2182132f5 Mon Sep 17 00:00:00 2001 From: Kayanski <44806566+Kayanski@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:33 +0200 Subject: [PATCH 05/10] Update .gitignore Co-authored-by: Mykhailo Donchenko <91957742+Buckram123@users.noreply.github.com> --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ce12d33f1..7452f6576 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ gen_cov_test docs/book -packages/integrations/**/artifacts \ No newline at end of file +packages/integrations/**/artifacts From d4abc0c972e9b3507bec4aa6a685e61ffdb11bfb Mon Sep 17 00:00:00 2001 From: Kayanski <44806566+Kayanski@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:40 +0200 Subject: [PATCH 06/10] Update before_publish.sh Co-authored-by: Mykhailo Donchenko <91957742+Buckram123@users.noreply.github.com> --- before_publish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/before_publish.sh b/before_publish.sh index a761b2991..46b58c0a5 100755 --- a/before_publish.sh +++ b/before_publish.sh @@ -1 +1 @@ -cargo run --example download-wasms -p cw-plus-orch \ No newline at end of file +cargo run --example download-wasms -p cw-plus-orch From 4b0fdf732de8b10c63677ff03500b463d5099c26 Mon Sep 17 00:00:00 2001 From: Kayanski <44806566+Kayanski@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:48 +0200 Subject: [PATCH 07/10] Update packages/integrations/cw-plus/Cargo.toml Co-authored-by: Mykhailo Donchenko <91957742+Buckram123@users.noreply.github.com> --- packages/integrations/cw-plus/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/integrations/cw-plus/Cargo.toml b/packages/integrations/cw-plus/Cargo.toml index c3a5a4d1e..661d6af28 100644 --- a/packages/integrations/cw-plus/Cargo.toml +++ b/packages/integrations/cw-plus/Cargo.toml @@ -2,11 +2,11 @@ name = "cw-plus-orch" version = "0.25.0" # Version of cw-orch that's used edition = "2021" -description = "CW4 implementation of group based on staked tokens" +description = "cw-orch interfaces for cw-plus base contracts" license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" -homepage = "https://cosmwasm.com" -documentation = "https://docs.cosmwasm.com" +repository = "https://github.com/AbstractSDK/cw-orchestrator" +homepage = "https://abstract.money" +documentation = "https://orchestrator.abstract.money" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 5a90c0c302e20774e41a1dc12c58d0fba6490e1c Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 18 Sep 2024 07:33:23 +0000 Subject: [PATCH 08/10] To Snake Case --- .../cw-plus/examples/{download-wasms.rs => download_wasms.rs} | 0 .../integrations/cw-plus/examples/{test-wasm.rs => test_wasm.rs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/integrations/cw-plus/examples/{download-wasms.rs => download_wasms.rs} (100%) rename packages/integrations/cw-plus/examples/{test-wasm.rs => test_wasm.rs} (100%) diff --git a/packages/integrations/cw-plus/examples/download-wasms.rs b/packages/integrations/cw-plus/examples/download_wasms.rs similarity index 100% rename from packages/integrations/cw-plus/examples/download-wasms.rs rename to packages/integrations/cw-plus/examples/download_wasms.rs diff --git a/packages/integrations/cw-plus/examples/test-wasm.rs b/packages/integrations/cw-plus/examples/test_wasm.rs similarity index 100% rename from packages/integrations/cw-plus/examples/test-wasm.rs rename to packages/integrations/cw-plus/examples/test_wasm.rs From f0f4a90cb65a97f0f953caef773e6cfb2f781929 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 18 Sep 2024 07:35:46 +0000 Subject: [PATCH 09/10] formatting --- packages/integrations/cw-plus/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/cw-plus/Cargo.toml b/packages/integrations/cw-plus/Cargo.toml index 661d6af28..940438667 100644 --- a/packages/integrations/cw-plus/Cargo.toml +++ b/packages/integrations/cw-plus/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cw-plus-orch" -version = "0.25.0" # Version of cw-orch that's used +version = "0.25.0" # Version of cw-orch that's used edition = "2021" description = "cw-orch interfaces for cw-plus base contracts" license = "Apache-2.0" From 3b83b6380d36dff3b44dbcd4384e2b491c8a6fa2 Mon Sep 17 00:00:00 2001 From: Kayanski Date: Wed, 18 Sep 2024 12:20:14 +0000 Subject: [PATCH 10/10] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f4890f86..de4c5b438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add a method on `TxHandler` to select instantiation permissions on Wasm upload - Adds an `upload_wasm` function to CosmosSender to upload wasm code associated to no Contract structure - Update syn to 2.0 +- Added cw-plus orchestrator interface to the repo. Pacing the way for more integrations inside this repository in the future ### Breaking