Cw-Orchestrator
cw-orchestrator is an advanced testing and deployment tool for CosmWasm smart-contracts. It’s designed to make it easy to test and deploy contracts in a variety of environments including cw-multi-test, local, testnet, and mainnet. It does this by providing the ability to write environment-generic code that interacts with CosmWasm contracts. In doing so it removes the need to maintain deployment code for multiple environments. In short, cw-orchestrator is the go-to tool for testing and deploying CosmWasm contracts.
Features
Below we briefly outline the key features that cw-orchestrator has to offer.
Testing
cw-orchestrator provides a testing framework that makes it easy to write tests for CosmWasm contracts. It does this by providing a testing environment that mimics the behavior of a CosmWasm blockchain. This allows you to write tests that interact with your contract in the same way that it would be interacted with on a real blockchain.
This framework allow developers to easily test contract-to-contract interactions without having to deal with the overhead of running a node locally. The testing framework also provides a number of utilities simplify the syntax for write tests. These utilities include the ability to easily set and query balances, set block height/time and more.
Additionally developers can share their infrastructure with other developers by creating a wrapper around their project’s deployment logic, allowing others to easily test how their contracts interact with the project.
The testing frameworks supported by cw-orchestrator includes:
Deployment + Scripting
cw-orchestrator also provides the ability to deploy to real networks. It does this by providing an easy to use interface to a blockchain node that can be used to submit transactions, query state and inspect transaction results. Any blockchain transaction can be broadcasted using cw-orchestrator.
Interface Generation
Interacting with a smart-contract is often verbose, leading to a lot of boilerplate code. cw-orchestrator solves this problem by providing a macro that generates an interface to a contract. This interface can be used to easily interact with a contract and improves the readability of your tests and deployments. Making it easier to write and maintain tests and deployments. Additionally, because this library is written in Rust, any breaking changes to your contract’s interface will cause a compile-time error, making it easy to keep your tests and deployments up to date.
Getting Started
These docs contain a quick-start and a longer tutorial-style walkthrough.
Quick-Start Guide
Get ready to change the way you interact with contracts. The following steps will allow you to write clean code such as:
#![allow(unused)] fn main() { counter.upload()?; counter.instantiate(&InstantiateMsg { count: 0 }, None, None)?; counter.increment()?; let count = counter.get_count()?; assert_eq!(count.count, 1); }
In this quick-start guide, we will review the necessary steps in order to integrate cw-orch
into a simple contract crate. We review integration of rust-workspaces (multiple contracts) at the end of this page.
NOTE: Additional content
If you’re moving quicker than everybody else, we suggest looking at a before-after review of this example integration. This will help you catch the additions you need to make to your contract to be able to interact with it using cw-orchestrator.
Summary
Single Contract Integration
Adding cw-orch
to your Cargo.toml
file
To use cw-orchestrator, you need to add cw-orch
to your contract’s TOML file. Run the command below in your contract’s directory:
$ cargo add --optional cw-orch
Alternatively, you can add it manually in your Cargo.toml
file as shown below:
[dependencies]
cw-orch = {version = "0.17.0", optional = true } # Latest version at time of writing
Now that we have added cw-orch
as an optional dependency we will want to enable it through a feature-flag. This ensures that the code added by cw-orch
is not included in the wasm artifact of the contract.
To do this add an interface
feature to the Cargo.toml
and enable cw-orch
when it is enabled like so:
[features]
interface = ["dep:cw-orch"] # Enables cw-orch when the feature is enabled
NOTE: If you are using
rust-analyzer
, you can add the following two lines in yoursettings.json
to make sure the features get taken into account when checking the project:"rust-analyzer.cargo.features": "all", "rust-analyzer.check.features": "all",
Creating an Interface
When using a single contract, we advise creating an interface.rs
file inside your contract’s directory. You then need to add this module to your lib.rs
file. Don’t forget to feature-flag the module in order to be able to use cw-orch
inside it.
#![allow(unused)] fn main() { #[cfg(feature = "interface")] mod interface; }
Then, inside that interface.rs
file, you can define the interface for your contract:
#![allow(unused)] fn main() { use cw_orch::{interface, prelude::*}; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] pub struct CounterContract; impl<Chain: CwEnv> Uploadable for CounterContract<Chain> { /// Return the path to the wasm file corresponding to the contract fn wasm(&self) -> WasmPath { artifacts_dir_from_workspace!() .find_wasm_path("counter_contract") .unwrap() } /// Returns a CosmWasm contract wrapper fn wrapper(&self) -> Box<dyn MockContract<Empty>> { Box::new( ContractWrapper::new_with_empty( crate::contract::execute, crate::contract::instantiate, crate::contract::query, ) .with_migrate(crate::contract::migrate), ) } } }
Learn more about the content of the interface creation specifics on the interface page
NOTE: It can be useful to re-export this struct to simplify usage (in
lib.rs
):#[cfg(feature = "interface")] pub use crate::interface::CounterContract;
Interaction helpers
cw-orchestrator provides a additional macros that simplify contract calls and queries. The macro implements functions on the interface for each variant of the contract’s ExecuteMsg
and QueryMsg
.
Enabling this functionality is very straightforward. Find your ExecuteMsg
and QueryMsg
definitions (in msg.rs
in our example) and add the ExecuteFns
and QueryFns
derive macros to them like below:
#![allow(unused)] fn main() { #[cw_serde] #[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] // Function generation /// Execute methods for counter pub enum ExecuteMsg { /// Increment count by one Increment {}, /// Reset count Reset { /// Count value after reset count: i32, }, } #[cw_serde] #[cfg_attr(feature = "interface", derive(cw_orch::QueryFns))] // Function generation #[derive(QueryResponses)] /// Query methods for counter pub enum QueryMsg { /// GetCount returns the current count as a json-encoded number #[returns(GetCountResponse)] GetCount {}, } // Custom response for the query #[cw_serde] /// Response from get_count query pub struct GetCountResponse { /// Current count in the state pub count: i32, } }
Find out more about the interaction helpers on the interface page
NOTE: Again, it can be useful to re-export these generated traits to simplify usage (in
lib.rs
):#[cfg(feature = "interface")] pub use crate::msg::{ExecuteMsgFns as CounterExecuteMsgFns, QueryMsgFns as CounterQueryMsgFns};
Using the integration
Now that all the setup is done, you can use your contract in tests, integration-tests or scripts.
Start by importing your crate, with the interface
feature enabled. Depending on your use-case this will be in [dependencies]
or [dev-dependencies]
:
counter-contract = { path = "../counter-contract", features = ["interface"] }
You can now use:
use counter_contract::{ msg::InstantiateMsg, CounterContract, CounterExecuteMsgFns, CounterQueryMsgFns, }; use cw_orch::{anyhow, prelude::*, tokio}; use tokio::runtime::Runtime; const LOCAL_MNEMONIC: &str = "clip hire initial neck maid actor venue client foam budget lock catalog sweet steak waste crater broccoli pipe steak sister coyote moment obvious choose"; pub fn main() -> anyhow::Result<()> { std::env::set_var("LOCAL_MNEMONIC", LOCAL_MNEMONIC); dotenv::dotenv().ok(); // Used to load the `.env` file if any pretty_env_logger::init(); // Used to log contract and chain interactions let rt = Runtime::new()?; let network = networks::LOCAL_JUNO; let chain = DaemonBuilder::default() .handle(rt.handle()) .chain(network) .build()?; let counter = CounterContract::new("counter_contract", chain); counter.upload()?; counter.instantiate(&InstantiateMsg { count: 0 }, None, None)?; counter.increment()?; let count = counter.get_count()?; assert_eq!(count.count, 1); Ok(()) }
Integration in a workspace
In this paragraph, we will use the cw-plus
repository as an example. You can review:
- The full integration code with
cw-orch
added - The complete diff that shows you all integration spots (if you want to go fast)
Handling dependencies and features
When using workspaces, you need to do the 2 following actions on all crates that include ExecuteMsg
and QueryMsg
used in your contracts:
- Add
cw-orch
as an optional dependency - Add an
interface
feature (ensurescw-orch
is not compiled into yourwasm
contract)
Refer above to Adding cw-orch
to your Cargo.toml
file for more details on how to do that.
For instance, for the cw20_base
contract, you need to execute those 2 steps on the cw20-base
contract (where the QueryMsg
are defined) as well as on the cw20
package (where the ExecuteMsg
are defined).
Creating an interface crate
When using workspace, we advise you to create a new crate inside your workspace for defining your contract’s interfaces. In order to do that, use:
cargo new interface --lib
cargo add cw-orch --package interface
Add the interface package to your workspace Cargo.toml
file
[workspace]
members = ["packages/*", "contracts/*", "interface"]
Inside this interface
crate, we advise to integrate all your contracts 1 by 1 in separate files. Here is the structure of the cw-plus
integration for reference:
interface (interface collection)
├── Cargo.toml
└── src
├── cw1_subkeys.rs
├── cw1_whitelist.rs
├── cw20_base.rs
├── cw20_ics20.rs
└── ..
When importing your crates to get the messages types, you can use the following command in the interface folder. Don’t forget to activate the interface feature to be able to use the cw_orch functionalities.
cargo add cw20-base --path ../contracts/cw20-base/ --features=interface
cargo add cw20 --path ../packages/cw20 --features=interface
Integrating single contracts
Now that you workspace is setup, you can integrate with single contracts using the above section
More examples and scripts
You can find more example interactions on the counter-contract
example directly in the cw-orchestrator
repo:
- Some examples showcase interacting with live chains.
- Some other examples show how to use the library for testing your contracts.
FINAL ADVICE: Continue to explore those docs to learn more about
cw-orch
. Why not go directly to environment variables?
Tutorial
This tutorial will guide you through setting up a single contract for use with cw-orchestrator. By the end of this tutorial you should be able to:
- Write deployment scripts for your contract.
- Write integration tests for your contract.
- Write executables for interacting with your contract.
In order to ensure that the code snippets shown here are correct we’ll be using the counter contract provided in the repository as the source for our code-snippets. You can find the contract here.
If you’re working within a cargo workspace environment you can follow along and read the Workspace docs after this tutorial.
Prerequisites
-
Contract Entry Point Messages: In order to use cw-orchestrator you need access to the entry point message types (
InstantiateMsg
,ExecuteMsg
,…) of the contracts you want to interact with. Having them locally will enable you to generate helper functions for interacting with the contracts. -
A gRPC endpoint (optional): If you want to perform on-chain transaction you will need access to the gRPC endpoint of a node. These are most-often available on port 9090. We provide chain definitions and constants for some of the more widely used Cosmos Chains. Learn more about this on the Daemon page.
-
A desire to learn (mandatory): This tutorial will cover the basics of using cw-orchestrator but it won’t cover everything. If you want to learn more about the features of cw-orchestrator you can check out the API Documentation.
The following sections detail setting up a contract, tests for the contract, and scripts for interacting with the contract on a blockchain network.
Following this example, the directory structure should eventually look like:
.
├── Cargo.toml
├── artifacts
│ └── counter.wasm (binary file)
└── counter
├── Cargo.toml
├── bin
│ └── deploy.rs
└── src
├── contract.rs (execute, instantiate, query, ...)
├── msg.rs (ExecuteMsg, QueryMsg, ...)
├── interface.rs (`cw-orch` contract structure definition)
└── ..
Sections
- Interfaces
- Define interfaces for your contracts.
- Environment File
- Configure your mnemonics, log settings and more.
- Scripting
- Write runnable scripts with your interfaces.
- Integration Tests
- Write an integration test for your contract.
Interfaces
Interfaces are virtual wrappers around CosmWasm contracts. They allow you to interact with your contracts in a type-safe way, and provide a convenient way to reason about contract interactions. Interfaces are the core reason why we built cw-orchestrator and we hope that you’ll find them as useful as we do.
Reminder: You can find the code for this example in the cw-orch counter-contract folder.
If you are a fast or visual learner, you can find a Before-After view of the
cw-orch
integration process in the sample contract.
Setup
Before we can create an interface we need to add cw-orch to the contract’s Cargo.toml
file. In counter
run:
$ cargo add --optional cw-orch
> Adding cw-orch v0.17.0 to optional dependencies.
or add it manually to the counter/Cargo.toml
file:
[dependencies]
cw-orch = {version = "0.17.0", optional = true } # Latest version at time of writing
We add cw-orch
as an optional dependency to ensure that it is not included in the wasm artifact of the contract. This way there are no trust assumptions made about the code added by cw-orch
, making it safe to use for production contracts.
However, we will need a way to enable the dependency when we want to use it. To do this add an interface
feature to the Cargo.toml
and enable cw-orch
when it is enabled.
You can do this by including the following in the counter/Cargo.toml
:
[features]
interface = ["dep:cw-orch"] # Adds the dependency when the feature is enabled
Features (aka. feature flags) are a way to enable or disable parts of your code. In this case, we are including cw-orch
as a dependency when the interface
feature is enabled. This is a common pattern for feature flags.
Creating an Interface
Now that we have our dependency set up we can create the interface. cw-orch
provides a powerful interface
macro that allows you to create an interface for your contract without having to call it at the entry points, as well as the ability to specify the contract’s source more easily.
To use this macro, first create a new file in the contract/src
directory called interface.rs
. This is where we will expose our interface.
In counter/src/lib.rs
:
#[cfg(feature = "interface")]
mod interface;
This allows importing the interface.rs
file only when the interface
feature is enabled on the contract. Again, this is done because we don’t want cw-orch dependencies to end up in your resulting Wasm
contract. Using this syntax won’t change anything to the actual wasm file that you are uploading on-chain if you don’t enable this feature by default.
Then in counter/src/interface.rs
:
use cw_orch::{interface, prelude::*};
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)]
pub struct CounterContract;
impl<Chain: CwEnv> Uploadable for CounterContract<Chain> {
/// Return the path to the wasm file corresponding to the contract
fn wasm(&self) -> WasmPath {
artifacts_dir_from_workspace!()
.find_wasm_path("counter_contract")
.unwrap()
}
/// Returns a CosmWasm contract wrapper
fn wrapper(&self) -> Box<dyn MockContract<Empty>> {
Box::new(
ContractWrapper::new_with_empty(
crate::contract::execute,
crate::contract::instantiate,
crate::contract::query,
)
.with_migrate(crate::contract::migrate),
)
}
}
This use of the interface
macro even allows you to have generic arguments in the message types. Any generics will be added to the interface under a PhantomData
attribute.
It can be beneficial to re-export the structure in our lib.rs
file.
In the counter contract we re-export in lib.rs
;
#[cfg(feature = "interface")]
pub use crate::interface::CounterContract;
NOTE: You can see that we have used the
artifacts_dir_from_workspace
macro inside thewasm
trait function. This macro helps you locate the workspaceartifacts
folder. It actually looks for any directory namedartifacts
from the root of the current crate going up. For instance if the project is located in/path1/path2/counter
, it will look for the artifacts folder inside the following directories in order and return as soon as it finds such a folder:
/path1/path2/counter
/path1/path2
/path1/
- …
This works for single contracts as well as workspace setups. If you have a specific setup, you can still specify the path yourself. If you do so, we advise indicating the wasm location from the current crate directory, using something like:
#![allow(unused)] fn main() { let crate_path = env!("CARGO_MANIFEST_DIR"); let wasm_path = format!("{}/../../artifacts/counter_contract.wasm", crate_path); WasmPath::new(wasm_path).unwrap() }
Constructor
The interface
macro implements a new
function on the interface:
// Construct the counter interface
let contract = CounterContract::new(CONTRACT_NAME, chain.clone());
The constructor takes two arguments:
contract_id
: The unique identifier for this contract. This is used as the key when retrieving address and code_id information for the contract. This argument is a&str
.chain
: The CosmWasm supported environment to use when calling the contract. Also includes the default sender information that will be used to call the contract. You can find more information later in the Integrations section for how to create thischain
variable
Interacting with your contracts
Now, you are able to interact directly with your contracts with ensured type safety.
The environments that are currently supported are:
- cw-multi-test by using
Mock
as thechain
variable. - Actual Cosmos SDK nodes for interacting with lives chains (
mainnet
,testnet
,local
). UseDaemon
as thechain
variable. - osmosis-test-tube or testing against actual chain binaries. This allows for fast testing with actual on-chain modules. This is particularly useful when testing against chain-specific modules. Use
OsmosisTestTube
as thechain
variable.
Generic functions
Generic functions can be executed over any environment. Setup functions are a good example of this.
/// Instantiate the contract in any CosmWasm environment
fn setup<Chain: CwEnv>(chain: Chain) -> anyhow::Result<CounterContract<Chain>> {
// Construct the counter interface
let contract = CounterContract::new(CONTRACT_NAME, chain.clone());
let admin = Addr::unchecked(ADMIN);
// Upload the contract
let upload_resp = contract.upload()?;
// Get the code-id from the response.
let code_id = upload_resp.uploaded_code_id()?;
// or get it from the interface.
assert_eq!(code_id, contract.code_id()?);
// Instantiate the contract
let msg = InstantiateMsg { count: 1i32 };
let init_resp = contract.instantiate(&msg, Some(&admin), None)?;
// Get the address from the response
let contract_addr = init_resp.instantiated_contract_address()?;
// or get it from the interface.
assert_eq!(contract_addr, contract.address()?);
// Return the interface
Ok(contract)
}
Entry Point Function Generation
Contract execution and querying is so common that we felt the need to improve the method of calling them. To do this we created two macros: ExecuteFns
and QueryFns
. As their name implies they can be used to automatically generate functions for executing and querying your contract through the interface.
Execution
To get started, find the ExecuteMsg
definition for your contract. In our case it’s located in counter/src/msg.rs
. Then add the following line to your ExecuteMsg
enum:
#[cw_serde]
#[cfg_attr(feature = "interface", derive(cw_orch::ExecuteFns))] // Function generation
/// Execute methods for counter
pub enum ExecuteMsg {
/// Increment count by one
Increment {},
/// Reset count
Reset {
/// Count value after reset
count: i32,
},
}
Again we feature flag the function generation to prevent cw-orchestrator entering as a dependency when building your contract.
The functions are implemented as a trait named ExecuteMsgFns
which is implemented on any interface that uses this ExecuteMsg
as an entrypoint message.
Using the trait then becomes as simple as:
// in integration_tests.rs
// Reset
use counter_contract::CounterExecuteMsgFns;
contract.reset(0)?;
Query
Generating query functions is a similar process but has the added advantage of using the cosmwasm-schema
return tags to detect the query’s return type. This allows for type-safe query functions!
#[cw_serde]
#[cfg_attr(feature = "interface", derive(cw_orch::QueryFns))] // Function generation
#[derive(QueryResponses)]
/// Query methods for counter
pub enum QueryMsg {
/// GetCount returns the current count as a json-encoded number
#[returns(GetCountResponse)]
GetCount {},
}
// Custom response for the query
#[cw_serde]
/// Response from get_count query
pub struct GetCountResponse {
/// Current count in the state
pub count: i32,
}
Using it is just as simple as the execution functions:
// in integration_tests.rs
// Get the count.
use counter_contract::CounterQueryMsgFns;
let count1 = contract.get_count()?;
// or query it manually
let count2: GetCountResponse = contract.query(&QueryMsg::GetCount {})?;
assert_eq!(count1.count, count2.count);
Just like the interface it can be beneficial to re-export the trait in your lib.rs
or interface.rs
file.
In the counter contract we re-export in lib.rs
;
#[cfg(feature = "interface")]
pub use crate::msg::{ExecuteMsgFns as CounterExecuteMsgFns, QueryMsgFns as CounterQueryMsgFns};
Additional Remarks on QueryFns
and ExecuteFns
The QueryFns
and ExecuteFns
derive macros generate traits that are implemented on any Contract structure (defined by the interface
macro) that have the matching execute and query types. Because of the nature of rust traits, you need to import the traits in your application to use the simplifying syntax. Those traits are named ExecuteMsgFns
and QueryMsgFns
.
Any variant of the ExecuteMsg
and QueryMsg
that has a #[derive(ExecuteFns)]
or #[derive(QueryFns)]
will have a function implemented on the interface (e.g. CounterContract
) through a trait. Here are the main things you need to know about the behavior of those macros:
- The function created will have the snake_case name of the variant and will take the same arguments as the variant.
- The arguments are ordered in alphabetical order to prevent attribute ordering from changing the function signature.
- If coins need to be sent along with the message you can add
#[payable]
to the variant and the function will take aVec<Coin>
as the last argument. - The
cw_orch::QueryFns
macro needs yourQueryMsg
struct to have thecosmwasm_schema::QueryResponses
macro implemented (this is good practice even outside of use withcw-orch
).
Additional configuration
payable
Attribute
Let’s see an example for executing a message (from a money market for instance).
money_market.deposit_stable()?;
There’s a problem with the above function. The money market only knows how much you deposit into it by looking at the funds you send along with the transaction. Cw-orchestrator doesn’t ask for funds by default. However, to allow attaching funds to a transaction, you can add the #[payable]
attribute on your enum variant like so:
#[derive(ExecuteFns)]
enum ExecuteMsg{
UpdateConfig{
config_field: String
},
#[payable]
DepositStable{}
...
}
Be defining this attribute, you can now use:
use cosmwasm_std::coins;
money_market.deposit_stable(&coins(456, "ujunox"))?;
fn_name
Attribute
#![allow(unused)] fn main() { #[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg{ Execute{ msg: CosmoMsg } } }
The following command will error because the execute
function is reserved for contract execution. This will not even compile actually.
#![allow(unused)] fn main() { money_market.execute(message_to_execute_via_a_proxy)?; }
This can happen in numerous cases actually, when using reserved keywords of cw-orch (or even rust). If this happens, you can use the fn_name
attribute to rename a generated function.
#![allow(unused)] fn main() { #[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg{ #[fn_name("proxy_execute")] Execute{ msg: CosmoMsg } } // This works smoothly ! money_market.proxy_execute(message_to_execute_via_a_proxy)?; }
This is also true for query functions.
impl_into
Attribute
For nested messages (execute and query) you can add an impl_into
attribute. This expects the enum to implement the Into
trait for the provided type. This is extremely useful when working with generic messages:
#![allow(unused)] fn main() { use cw_orch::interface; use cw_orch::prelude::*; // An execute message that is generic. #[cosmwasm_schema::cw_serde] pub enum GenericExecuteMsg<T> { Generic(T), Nested(NestedMessageType), } // This is the message that will be used on our contract type ExecuteMsg = GenericExecuteMsg<Foo>; #[cosmwasm_schema::cw_serde] #[derive(cw_orch::ExecuteFns)] #[impl_into(ExecuteMsg)] pub enum Foo { Bar { a: String }, } impl From<Foo> for ExecuteMsg { fn from(msg: Foo) -> Self { ExecuteMsg::Generic(msg) } } #[cosmwasm_schema::cw_serde] #[derive(cw_orch::ExecuteFns)] #[impl_into(ExecuteMsg)] pub enum NestedMessageType { Test { b: u64 }, } impl From<NestedMessageType> for ExecuteMsg { fn from(msg: NestedMessageType) -> Self { ExecuteMsg::Nested(msg) } } #[interface(Empty, ExecuteMsg, Empty, Empty)] struct Example<Chain>; impl<Chain: CwEnv> Example<Chain> { pub fn test_macro(&self) { // function `bar` is available because of the `impl_into` attribute! self.bar("hello".to_string()).unwrap(); // function `test` is available because of the `impl_into` attribute! self.test(65).unwrap(); } } }
disable_fields_sorting
Attribute
By default the ExecuteFns
and QueryFns
derived traits will sort the fields of each enum member. For instance,
#![allow(unused)] fn main() { #[cw_serde] #[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsgOrdered { Test { b: u64, a: String }, } }
will generate
#![allow(unused)] fn main() { pub fn bar(a: String, b: u64) -> ...{ ... } }
You see in this example that the fields of the bar function are sorted lexicographically. We decided to put this behavior as default to prevent potential errors when rearranging the order of enum fields. If you don’t want this behavior, you can disable it by using the disable_fields_sorting
attribute. This is the resulting behavior:
#![allow(unused)] fn main() { #[cw_serde] #[derive(cw_orch::ExecuteFns)] #[disable_fields_sorting] pub enum ExecuteMsg { Test { b: u64, a: String }, } pub fn bar(b: u64, a: String) -> ...{ ... } }
NOTE: This behavior CAN be dangerous if your struct members have the same type. In that case, if you want to rearrange the order of the members inside the struct definition, you will have to be careful that you respect the orders in which you want to pass them.
Learn more
Got questions? Join the Abstract Discord and ask in the #cw-orchestrator
channel.
Learn more about Abstract at abstract.money.
References
Environment Variables
cw-orch leverages some environment variables to interact with contracts on actual blockchains. The environment variables are described here. You can find additional information about their usage, default values and types in the cw-orch
repo.
IMPORTANT: Before proceeding, ensure that you add .env
to your .gitignore
. We are not responsible for any loss of funds due to leaked mnemonics.
Here are the optional environment variables:
# .env
# info, debug, trace (if using env_logger for logging)
RUST_LOG=info
# Where the contract wasms are located (used by ArtifactsDir::env())
ARTIFACTS_DIR="../artifacts"
# where to store the state of your deployments (default: ./state.json)
STATE_FILE="./my_state.json"
# Mnemonics of the account that will be used to sign transactions
# Can optionally be set on DaemonBuilder as well.
MAIN_MNEMONIC="" # Necessary if interacting with a cw-orch-daemon on mainnet
TEST_MNEMONIC="" # Necessary if interacting with a cw-orch-daemon on testnet
LOCAL_MNEMONIC="" # Necessary if interacting with a cw-orch-daemon locally
## Additional configuration variables. These are optional. We show default values here:
# Optional - Float. This allows changing the gas buffer applied after tx simulation
CW_ORCH_GAS_BUFFER = 1.3
# Optional - Integer. This changes the number of tx queries before it fails if it doesn't find any result
CW_ORCH_MAX_TX_QUERY_RETRIES = 50
# Optional - Integer. Minimum block speed in seconds. Useful when the block speeds are varying a lot
CW_ORCH_MIN_BLOCK_SPEED = 1
# Optional - String. If equals to "true", will serialize the blockchain messages as json (for easy copying) instead of Rust Debug formatting
CW_ORCH_SERIALIZE_JSON = "false"
# Optional - Absolute Path. Sets the directory where the state file will be saved.
# This is not enforced to be an absolute path but this is highly recommended
CW_ORCH_STATE_FOLDER = "~/.cw-orchestrator"
Mnemonics
Only 24-word mnemonics are supported at this time. If you’re experienced with keychain and private key management we’d really appreciate your help in adding support for other formats. Please reach out to us on Discord if you’re interested in helping out.
Writing and Executing Scripts
Now that we have the interface written for our contract, we can start writing scripts to deploy and interact with it on a real blockchain. We’ll do this by adding a examples
folder in the counter contract and add our deploy script there. We only provide an overview of how scripting can be done here. Find our more about how to make your scripting dreams come true on the Daemon page.
Setup
Before we get going we need to add the examples
folder and tell cargo that it contains scripts. We can do this by creating a folder named examples in counter
and creating a file in it called deploy.rs
mkdir counter/examples
touch counter/examples/deploy.rs
Then we want to add the required dependencies to the dev-dependencies
section of our Cargo.toml
file. We’ll need dotenv
to load our environment variables and pretty_env_logger
to log to stdout. We’re using examples
instead of bin
because setting a feature on an optional dependency is not supported.
[dev-dependencies]
# Deps for deployment
dotenv = { version = "0.15.0" } # Enables loading of .env files
pretty_env_logger = { version = "0.5.0" } # Enables logging to stdout and prettifies it
counter-contract = { path = ".", features = ["interface"] } # Allows to activate the `interface` feature for examples and tests
Now we’re ready to start writing our script.
Main Function
With the setup done, we can start writing our script. Our initial plan is to deploy the counter contract to the chain. We’ll start by writing a main function that will call our deploy function.
We start by creating the chain object for specifying we want to interact with a local juno instance:
dotenv::dotenv().ok(); // Used to load the `.env` file if any
pretty_env_logger::init(); // Used to log contract and chain interactions
let rt = Runtime::new()?;
let network = networks::LOCAL_JUNO;
let chain = DaemonBuilder::default()
.handle(rt.handle())
.chain(network)
.build()?;
Then we can interact with our contract using that chain
let counter = CounterContract::new("counter_contract", chain);
counter.upload()?;
counter.instantiate(&InstantiateMsg { count: 0 }, None, None)?;
counter.increment()?;
let count = counter.get_count()?;
assert_eq!(count.count, 1);
Asynchronous Daemon
All the functionalities described in this guide/tutorial only allow for synchronous interactions. If for whatever reason you need to interact with an actual chain in an asynchronous way, you can use the DaemonAsync
structure. However, this structure won’t allow for much interoperability as it’s not compatible with the cw-orch
Contract structure. Blockchain transactions have to be sequential because of the sequence
of an account and that’s why we provide limited support to asynchronous transaction broadcasting capabilities.
Integration Tests
Integration tests are very easy to write with cw-orch and are 100% compatible with actual on-chain deployment and scripting. We provide an overview of how they can be executed here. Find our more about how to setup your integration tests on the Cw Multi Test page
Start by creating a tests
folder in your contract’s dir.
mkdir counter/tests
Then create a file called integration_tests.rs
in the tests
folder.
touch counter/tests/integration_tests.rs
Now we can write our tests. Here’s an example of a test that deploys the contract, increments the counter and then resets it.
use counter_contract::{
contract::CONTRACT_NAME,
msg::{GetCountResponse, InstantiateMsg, QueryMsg},
ContractError, CounterContract,
};
// Use prelude to get all the necessary imports
use cw_orch::prelude::*;
use cosmwasm_std::Addr;
// consts for testing
const USER: &str = "user";
const ADMIN: &str = "admin";
#[test]
fn count() -> anyhow::Result<()> {
// Create a sender
let sender = Addr::unchecked(ADMIN);
// Create a user
let user = Addr::unchecked(USER);
// Create the mock. This will be our chain object throughout
let mock = Mock::new(&sender);
// Set up the contract (Definition below) ↓↓
let contract = setup(mock.clone())?;
// Increment the count of the contract
contract
// Set the caller to user
.call_as(&user)
// Call the increment function (auto-generated function provided by CounterExecuteMsgFns)
.increment()?;
// Get the count.
use counter_contract::CounterQueryMsgFns;
let count1 = contract.get_count()?;
// or query it manually
let count2: GetCountResponse = contract.query(&QueryMsg::GetCount {})?;
assert_eq!(count1.count, count2.count);
// Check the count
assert_eq!(count1.count, 2);
// Reset
use counter_contract::CounterExecuteMsgFns;
contract.reset(0)?;
let count = contract.get_count()?;
assert_eq!(count.count, 0);
// Check negative case
let exec_res: Result<cw_multi_test::AppResponse, CwOrchError> =
contract.call_as(&user).reset(0);
let expected_err = ContractError::Unauthorized {};
assert_eq!(
exec_res.unwrap_err().downcast::<ContractError>()?,
expected_err
);
Ok(())
}
/// Instantiate the contract in any CosmWasm environment
fn setup<Chain: CwEnv>(chain: Chain) -> anyhow::Result<CounterContract<Chain>> {
// Construct the counter interface
let contract = CounterContract::new(CONTRACT_NAME, chain.clone());
let admin = Addr::unchecked(ADMIN);
// Upload the contract
let upload_resp = contract.upload()?;
// Get the code-id from the response.
let code_id = upload_resp.uploaded_code_id()?;
// or get it from the interface.
assert_eq!(code_id, contract.code_id()?);
// Instantiate the contract
let msg = InstantiateMsg { count: 1i32 };
let init_resp = contract.instantiate(&msg, Some(&admin), None)?;
// Get the address from the response
let contract_addr = init_resp.instantiated_contract_address()?;
// or get it from the interface.
assert_eq!(contract_addr, contract.address()?);
// Return the interface
Ok(contract)
}
Workspace Tutorial
WIP
Following this example, the project’s structure should look like:
.
├── Cargo.toml
├── artifacts
│ ├── other_contract.wasm
│ └── my_contract.wasm
├── contracts
│ ├── my-contract
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── contract.rs (execute, instantiate, query, ...)
│ │ └── ..
│ └── other-contract
│ └── ..
├── packages
│ ├── my-project (messages)
│ │ ├── Cargo.toml
│ │ └── src
│ │ ├── lib.rs
│ │ ├── my-contract.rs
│ │ ├── other-contract.rs
│ │ └── ..
│ └── my-project-interface (interface collection)
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ ├── my-project.rs
│ └── ..
└── scripts (executables)
├── .env
├── Cargo.toml
└── src
├── deploy.rs
└── test_project.rs
Integrations
cw-orchestrator aims at allowing developers to interact with different execution environments with the exact same code base and syntax.
In order to do so, it provides a set of traits and interfaces, e.g. for contracts that can be implemented on execution structures. As of writing this documentation, the following execution environments are supported:
Those environments allow developers to test, deploy, manage, migrate, maintain, without having to switch programming languages, care for types and version compatibilities as they are enforced by the compiler and even change the syntax of the code used.
The Unit Tests page shows you how you can unit-test your contracts against actual on-chain data!
Daemon
Daemon
is a CosmWasm execution environment for interacting with CosmosSDK chains. The Daemon
allows you to deploy/migrate/configure your contracts on main and testnets as well as locally running chain instances. Furthermore it provides a wide range of tools to interact with the chains. We describe those tools in depth in this page.
Quick Start
Interacting with the daemon
is really straightforward. How to creating a daemon instance is shown below:
use cw_orch::prelude::*;
use tokio::runtime::Runtime;
// We start by creating a runtime, which is required for a sync daemon.
let runtime = Runtime::new().unwrap();
// We can now create a daemon. This daemon will be used to interact with the chain.
let daemon = Daemon::builder()
// set the network to use
.chain(cw_orch::daemon::networks::LOCAL_JUNO) // chain parameter
.handle(runtime.handle()) // handler parameter
.build()
.unwrap();
-
The
chain
parameter allows you to specify which chain you want to interact with. The chains that are officially supported can be found in thecw_orch::daemon::networks
module. You can also add additional chains yourself by simply defining a variable of typeChainInfo
and using it in your script. Don’t hesitate to open a PR on the cw-orchestrator repo, if you would like us to include a chain by default. The variables needed for creating the variable can be found in the documentation of the chain you want to connect to or in the Cosmos Directory. -
The
handle
parameter is a tokio runtime handle.If you don't know what that means
You don’t need to know all the intricacies of tokio and rust-async to use daemons. If you don’t have time to learn more, you can simply use the snippet above and not touch the runtime creation. However for more advanced
daemon
usage and also for your culture, don’t hesitate to learn about those wonderful processes and their management.If you know what that means
This handler is used because all the front-facing daemon methods are synchronous. However everything that’s happening in the background is asynchronous. This handle is used exclusively to await asynchronous function:
runtime.block_on(...)
Because creating runtimes is a costly process, we leave the handler creation and management to the user.
This simple script actually hides another parameter which is the LOCAL_MNEMONIC
environment variable. This variable is used when interacting with local chains. See the part dedicated to Environment Vars for more details.
NOTE: When using
daemon
, you are interacting directly with a live chain. The program won’t ask you for your permission at each step of the script. We advise you to test ALL your deployments on test chain before deploying to mainnet.
Interacting with contracts
You can then use the resulting Daemon
variable to interact with your contracts:
let counter = CounterContract::new("local:counter", daemon.clone());
let upload_res = counter.upload();
assert!(upload_res.is_ok());
let init_res = counter.instantiate(
&InstantiateMsg { count: 0 },
Some(&counter.get_chain().sender()),
None,
);
assert!(init_res.is_ok());
All contract operations will return an object of type cw_orch::prelude::CosmTxResponse
. This represents a successful transaction. Using the txhash of the tx, you can also inspect the operations on a chain explorer.
ADVICE: Add
RUST_LOG=INFO
to your environment and use theenv_logger::init()
initializer to get detailed information about your script execution. Cw-orchestrator provides enhanced logging tools for following the deployment and potentially pick up where you left off. This environment needs wasm artifacts to deploy the contracts to the chains. Don’t forget to compile all your wasms before deploying your contracts !
State management
In order to manage your contract deployments cw-orchestrator saves the contract addresses and code ids for each network you’re interacting with in a JSON formatted state file. This state file represents all your past. You can customize the path to this state file using the STATE_FILE
env variable.
When calling the upload
function on a contract, if the tx is successful, the daemon will get the uploaded code_id and save it to file, like so:
{
"juno": {
"juno-1": {
"code_ids": {
"counter_contract": 1356,
},
}
}
}
In this example: counter_contract
corresponds to the contract_id
variable (the one that you can set in the contract interface constructor).
When calling the instantiate
function, if the tx is successful, the daemon will get the contract address and save it to file, like so:
{
"juno": {
"juno-1": {
"code_ids": {
"counter_contract": 1356,
},
"default": {
"counter_contract": "juno1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqwrw37d"
}
}
}
}
In this example, the default
keyword corresponds to the deployment namespace. This can be set when building the daemon object (using the DaemonBuilder::deployment_id
method) in order to separate multiple deployments. For instance for a DEX (decentralized exchange), you can have a single code-id but multiple pool addresses for all your liquidity pools. You would have a juno-usdc
and a usdt-usdc
deployment, sharing the same code-ids but different contract instances.
Additional tools
The Daemon
environment provides a bunch of tools for you to interact in a much easier way with the blockchain. Here is a non-exhaustive list:
-
Send usual transactions:
#![allow(unused)] fn main() { let wallet = daemon.wallet(); rt.block_on(wallet.bank_send("<address-of-my-sister>", coins(345, "ujunox")))?; }
-
Send any transaction type registered with
cosmrs
:#![allow(unused)] fn main() { let tx_msg = cosmrs::staking::MsgBeginRedelegate { /// Delegator's address. delegator_address: AccountId::from_str("<my-address>").unwrap(), /// Source validator's address. validator_src_address: AccountId::from_str("<my-least-favorite-validator>").unwrap(), /// Destination validator's address. validator_dst_address: AccountId::from_str("<my-favorite-validator>").unwrap(), /// Amount to UnDelegate amount: Coin { amount: 100_000_000_000_000u128, denom: Denom::from_str("ujuno").unwrap(), }, }; rt.block_on(wallet.commit_tx(vec![tx_msg.clone()], None))?; }
-
Send any type of transactions (Using an
Any
type):#![allow(unused)] fn main() { rt.block_on(wallet.commit_tx_any( vec![cosmrs::Any { type_url: "/cosmos.staking.v1beta1.MsgBeginRedelegate".to_string(), value: tx_msg.to_any().unwrap().value, }], None, ))?; }
-
Simulate a transaction without sending it
#![allow(unused)] fn main() { let (gas_needed, fee_needed) = rt.block_on(wallet.simulate(vec![tx_msg.to_any().unwrap()], None))?; log::info!( "Submitting this transaction will necessitate: - {gas_needed} gas - {fee_needed} for the tx fee" ); }
Queries
The daemon object can also be used to execute queries to the chains we are interacting with. This opens up a lot more applications to cw-orchestrator as this tools can also be used to manage off-chain applications.
Querying the chain for data using a daemon looks like:
let bank_query_client: Bank = daemon.query_client();
let sender = "valid_sender_addr";
let balance_result = bank_query_client.balance(sender, None).await?;
println!("Balance of {} : {:?}", sender, balance_result);
For more information and queries, visit the daemon querier implementations directly
Example of code leveraging Daemon capabilities
Here is an example of a script that deploys the counter contract only after a specific block_height.
impl CounterContract<Daemon> {
/// Deploys the counter contract at a specific block height
pub fn await_launch(&self) -> Result<()> {
let daemon = self.get_chain();
let rt = daemon.rt_handle.clone();
rt.block_on(async {
// Get the node query client, there are a lot of other clients available.
let node = daemon.query_client::<Node>();
let mut latest_block = node.latest_block().await.unwrap();
while latest_block.header.height.value() < 100 {
// wait for the next block
daemon.next_block().unwrap();
latest_block = node.latest_block().await.unwrap();
}
});
let contract = CounterContract::new(CONTRACT_NAME, daemon.clone());
// Upload the contract
contract.upload().unwrap();
// Instantiate the contract
let msg = InstantiateMsg { count: 1i32 };
contract.instantiate(&msg, None, None).unwrap();
Ok(())
}
}
CW Multi Test
Cw Multi Test is a rust-based test framework that allows developers to test for multi-contract interactions without having to dispatch messages, storage and other variables themselves. With cw-orchestrator, most of the cw-multi-test
logic is abstracted away, but you can still learn more about the framework here.
Quick Start
The cw-multi-test
integration comes at no extra cost for the developer. Creating a test environement in cw-orchestrator that leverages cw-multi-test
goes along the lines of:
use cw_orch::prelude::*;
use cosmwasm_std::Addr;
let sender = Addr::unchecked("juno16g2rahf5846rxzp3fwlswy08fz8ccuwk03k57y");
let mock = Mock::new(&sender);
NOTE: When using
cw-multi-test
, the addresses ARE NOT validated like on a live chain. Therefore, you can use any string format for designating addresses. For instance,Addr::unchecked("my-first-sender")
is a validcw-multi-test
address.
NOTE: When using
cw-multi-test
, NO gas fees are charged to the sender address.
Interacting with contracts
You can then use the resulting Mock
variable to interact with your contracts:
let contract_counter = CounterContract::new("mock:contract_counter", mock);
let upload_res = contract_counter.upload();
upload_res.unwrap();
When executing contracts in a cw-multi-test
environment, the messages and sub-messages sent along the Response of an endpoint, will be executed as well.
This environment mocks the actual on-chain execution exactly
This environment uses the actual functions of your contract without having to compile them into WASM. When you are calling
upload
with this environment, no wasm files are included in the test environment. This allows for better debugging of your contract code.
If you are using the customizable Interface Macro, you will need to have implemented the
wrapper
function for interacting the theMock
environment. This function wil allow you to “connect” your contract endpoints to yourContract
struct See the dedicated page for more details.
NOTE: Keep in mind that
cw-multi-test
is based solely in rust and that a lot of actual blockchain modules are not mocked in the environment. The main cosmos modules are there (Bank, Staking), but some very useful ones (tokenfactory, ibc) as well as Stargate messages are not supported by the environment.
Cloning
When cloning a cw_multi_test environment, you are not cloning the entire environment, but instead you are creating a new Mock
typed variable with the same underlying cw_multi_test::App
object reference. This is useful for objects that require to pass the chain as an object rather than by reference.
The underlying cw_multi_test::App
object is however not clonable.
Additional tools
The Mock
test environment allows you to change application variables (such as the balance of an account) using wrappers around the underlying cw_multi_test::App
object. Here are some examples of those wrappers in context:
let new_sender = Addr::unchecked("entirely-new-sender");
mock.set_balance(&new_sender, coins(100_000, "ujunox"))
.unwrap();
// Reuploads as the new sender
contract_counter.call_as(&new_sender).upload().unwrap();
// Here the contract_counter sender is again `sender`
// Sets the new_sender as the definite sender
contract_counter.set_sender(&new_sender);
// From now on the contract_counter sender is `new_sender`
Additional customization
As we don’t provide wrappers around each and every functionality that cw-multi-test
provides, you can also customize the underlying cw_multi_test::App
object to your specific needs. In the following example, we create a new validator in the test environment:
mock.app
.borrow_mut()
.init_modules(|router, api, storage| {
router.staking.add_validator(
api,
storage,
&BlockInfo {
height: 16736,
time: Timestamp::from_seconds(13345762376),
chain_id: "juno-1".to_string(),
},
cosmwasm_std::Validator {
address: "new-validator-address".to_string(),
commission: Decimal::from_str("0.5").unwrap(), // Greedy validator
max_commission: Decimal::from_str("1").unwrap(), // Dangerous validator
max_change_rate: Decimal::from_str("1").unwrap(), // Very dangerous validator
},
)
})
.unwrap();
Osmosis Test Tube
Osmosis Test Tube is a rust-based test framework that allows developers to test for multi-contract interactions without having to dispatch messages, storage and other variables themselves. This environment is even closer to the actual on-chain mechanisms than Cw Multi Test, because it runs tests directly on the actual chain binaries. With cw-orchestrator, most of the osmosis-test-tube
logic is abstracted away, but you can still learn more about the framework here.
Prerequesites
In order to use osmosis-test-tube
, the library needs to be able to compile and install the chain binaries. For that you will need to install a go compiler as well as the clang library. Run the following commands to install both of those libraries.
Ubuntu
- Install go
- Install the clang library
sudo apt install clang
Arch Linux
- Install go
sudo pacman -Sy go
- Instal the clang library
sudo pacman -Sy clang
Quick Start
Creating a test environment in cw-orchestrator that leverages osmosis-test-tube
goes along the lines of:
use cw_orch::prelude::*;
use cosmwasm_std::coins;
let chain = OsmosisTestTube::new(coins(1_000_000_000_000, "uosmo"));
This snippet will create a new address, provide it with initial balances and create the osmosis-test-tube
environment.
The addresses are not handled like in the cw-multi-test
environment or in mock tests and can’t be decided upon manually. You will learn more later about handling addresses in the OsmosisTestTube environement.
NOTE: When using
osmosis-test-tube
, the addresses are validated like on a live chain.
NOTE: When using
osmosis-test-tube
, gas fees are charged to the sender address. The gas fees don’t represent the actual gas fees you will occur when interacting with the actual chain. That’s why in the test snippet above, the amount ofuosmo
instantiated with the account is very high.
Interacting with contracts
You can then use the resulting OsmosisTestTube
variable to interact with your contracts:
let contract_counter = CounterContract::new("osmosis_test_tube:contract_counter", chain);
let upload_res = contract_counter.upload();
assert!(upload_res.is_ok());
let init_res = contract_counter.instantiate(&InstantiateMsg { count: 0 }, None, None);
assert!(init_res.is_ok());
When executing contracts in an osmosis_test_tube
environment, the messages and sub-messages sent along the Response of an endpoint, will be executed as well. This environment mimics the actual on-chain execution by dispatching the messages inside the actual chain binaries.
This environment uses wasm files compiled from the project. Therefore, you need to compile the WASM artifacts from the project for your osmosis-test-tube integration tests. If you are using the customizable Interface Macro, you will need to have implemented the
wasm
function. See the dedicated page for more details.
Cloning
When cloning an osmosis_test_tube
environment, you are not cloning the entire environment, but instead you are creating a new OsmosisTestTube
typed variable with the same underlying osmosis_test_tube::App
object reference. This is useful for objects that require to pass the chain as an object rather than by reference.
The underlying osmosis_test_tube::App
object is however not clonable.
Additional tools
The OsmosisTestTube
test environment allows you to change application variables (such as the balance of an account) using wrappers around the underlying osmosis_test_tube::App
object. Here are some examples of those wrappers in context:
let new_sender = chain.init_account(coins(100_000, "ujunox")).unwrap();
// Reuploads as the new sender
contract_counter.call_as(&new_sender).upload().unwrap();
// Here the contract_counter sender is again `sender`
// Sets the new_sender as the definite sender
contract_counter.set_sender(&new_sender);
// From now on the contract_counter sender is `new_sender`
NOTE: With OsmosisTestTube, you can’t create accounts with a specific address. Accounts are created by the app directly and you don’t have a lot of control over them. As in the example, use
OsmosisTestTube::init_account
to create a new account.
Additional customization
As we don’t provide wrappers around each and every functionality that osmosis_test_tube
provides, you can also customize the underlying osmosis_test_tube::App
object to your specific needs. In the following example, we increase the block time in the test environment:
chain.app.borrow_mut().increase_time(150);
Unit Tests
Cw-orchestrator provides an additional tool to help you with unit tests with context. The WasmMockQuerier
object allows you to leverage cw-orch
querying abilities to unit-test your contracts.
Capabilities
The WasmMockQuerier
(and associated mock_dependencies
helper function) allow developers to unit-test their contract against on-chain logic. This allows for a mix between unit and integration tests for application that need to rely on on-chain data. This structure works very similarly to cosmwasm_std::testing::mock_dependencies
but instead of querying for local scripts or data, it queries the information from an actual running blockchain.
Today, the following modules are supported by this querier:
- Wasm
- Bank
- Staking (support has not been finalized as of yet)
Example
Let’s imagine you want to build a lending aggregator. In this kind of application, you want to query the balance in staking tokens that your client has. In order to do that, you may want to use the following syntax inside your contract:
#![allow(unused)] fn main() { fn query_balance(deps: Deps, client: Addr, lending_contract: Addr) -> Result<BalanceResponse, ContractError>{ let balance: BalanceResponse = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart{ contract_addr: lending_contract, query: Binary::from({ StakingBalance{ ... } }) }))?; } }
In order to unit-test that logic, you may want to test against actual on-chain data. The following code_snippet will allow you to test this piece of code against actual on-chain code.
#![allow(unused)] fn main() { #[test] fn balance_is_available() -> anyhow::Result<()>{ let deps = cw_orch::prelude::live_mock::mock_dependencies(JUNO_1); // assert the query is successful let balance_response = query_balance(deps.as_ref(), Addr::unchecked("juno1..."), Addr::unchecked("juno1..")).unwrap(); } }
Obviously, you need to specify existing on-chain addresses to be able to use the cw_orch
mock_dependencies
function. Those dependencies have a very similar behavior to cosmwasm_std::testing::mock_dependencies
, expect the query response is fetched from on-chain information.
Fork Testing
NOTE: This feature is not publicly available yet. If you want preview access to this testing feature, please reach out via our website.
Cw-orchestrator supports testing in a forked environment. With this feature, you can execute the application you are developing on a blockchain environment without having to spin up a node yourself and create a fork locally. All the code and application you will be running during the test will be rust code and the storage needed to execute is minimal as only the necessary data is downloaded from actual blockchain nodes.
Brief Overview
We leverage the beautiful cw-multi-test
package, created and maintained by Cosmwasm and added a few functionalities that allow you to execute your code just as if you were interacting with an actual on-chain node, but locally, and without any on-chain funds necessary.
Setup
Setting up the environment is really easy and only requires feeding a ChainData
object to the struct constructor:
#![allow(unused)] fn main() { use cw_orch::networks::JUNO_1; use cw_orch::ForkMock; let app = ForkMock::new(JUNO_1)?; }
With this, you are ready to upload, instantiate, migrate and interact with on-chain contracts…
Execution Flow
This execution environment has a mixed behavior.
Execution
The blockchain modules logic is implemented in rust. Every module that is not standard has to be present and implemented in rust to be available. Because we are based on cw-multi-test
, our application is compatible with cw-multi-test modules. For now:
- The Wasm module is implemented locally via the
cosmwasm-vm
package that handles wasm execution. - The Bank module is implemented by
cw-multi-test
and available in this environment. - The Staking module is not fully implemented because distant storage is more difficult to query. It’s the next module we wish to implement.
Storage
This part is actually at the center of the innovation provided by this package.
-
When reading blockchain storage, the testing environment will execute in order:
- Check local storage for data availability. If some data was stored locally, use that value
- If not, check the registered blockchain node for data availability and error if it’s not available.
-
When writing value to storage, nothing changes.
Let’s take an example for clarity. Say I want to deposit some funds into Anchor Protocol. Here are the steps that a user would have to go through and how they are executed inside the environment.
-
The user needs funds to interact with the protocol. A
fork-helper
allows to increase the balance of an address.#![allow(unused)] fn main() { // Sender address. Can also be an actual account with funds. // Could also be app.sender() for creating an address automatically. let sender = "terra1..."; let sender_addr = Addr::unchecked(sender); app.set_sender(&sender_addr); // We add some funds specific for our application app.set_balance(&sender_addr, coins(10_000_000, "uusd"))?; }
-
The user calls the following
ExecuteMsg
on the actual mainnet Anchor Moneymarket Contract:#![allow(unused)] fn main() { let market_addr = Addr::unchecked("terra1..."); // Actual contract address of the Anchor deployment. let market = AnchorMarket::new("anchor:money-market", app.clone()); market.set_address(&market_addr); market.deposit_stable(&coins(10_000, "uusd"))?; }
-
During the whole message execution, when storage is queried, if it doesn’t exist locally it will be queried from the chain. This is true for storage during contract execution but this is also true for querying the actual Wasm Code when executing/querying a contract. No local storage is used until something is written to it 1.
-
Even in the case of multiple chained contract calls, storage is modified accordingly and usable by contracts.
-
After message execution, queries and states are modified according to the contract execution. After depositing, it is now possible to query your stake or even to withdraw your funds:
#![allow(unused)] fn main() { let a_currency = "terra1..."; // The contract address for the staking receipt /// This should give a non-zero value, even if no change occurred on the actual mainnet state let response: BalanceResponse = app .query(&Cw20QueryMsg::Balance { address: sender.to_string(), }, &Addr::unchecked(a_currency), )?; /// We can get our funds back, no problem, the state changes get propagated as well locally market.redeem_all_stable()?; }
Usage
You use this fork environment as you would use the Mock
environment, with a few subtle changes:
-
You can’t use human readable addresses, because this environment uses actual APIs and needs to be able to verify addresses. When creating the Mock environment, a sender gets created along and attach automatically to the
ForkMock
instance. If you need additional addresses, you can use:#![allow(unused)] fn main() { let new_sender: Addr = fork.init_account(); }
-
The environment doesn’t allow (yet) contracts to be defined using its functions (just like the
Mock
can). A contract in this environment is executed through its compiled wasm. When testing with this environment, make sure that you compile your project before running the tests2.
1 In the future, we might leverage a local storage cache to avoid querying distant RPCs too much (for more speed and less data consumption).
2 We are aware that this is not very easy on the user and we are working on being able to accept both structures that implement the Contract
trait AND wasm files for testing with this for environment.
Interchain Capabilities
Because of its asynchronous and decentralized nature, the Inter-Blockchain Communication protocol makes developing and debugging applications more difficult than simple blockchain transactions. Cw-orch simplifies those tasks by providing developers tools, full testing environments and standard interfaces to interact with applications that leverage IBC capabilities.
Here are a few examples of what cw-orchestrator allows:
Interchain packet following
Using some simple tools, one can follow the execution of IBC packets through their whole lifetime (Receive, Acknowledge or Timeout). This is mostly useful for packet analysis of certain channels, ports or connections.
#![allow(unused)] fn main() { let packet_lifetime = interchain.follow_packet( "juno", "transfer", "channel-16", "akash", 3u64.into() ).await?; }
Interchain transaction waiting
To further simplify the developer experience and provide simple syntax, cw-orchestrator allows developers to await execution of transactions that create IBC packets. This is mostly useful when interacting with existing contracts in a script to simplify the execution pipeline.
#![allow(unused)] fn main() { // We order an ICA controller to burn some funds on juno from akash // Upon submitting this transaction successfully, a packet will be sent from akash to juno to trigger the ICA action let transaction_response = controller.send_msgs("channel-16", vec![ CosmosMsg::Bank(cosmwasm_std::BankMsg::Burn { amount: vec![cosmwasm_std::coin(100u128, "ujuno")], }) ])?; // This function won't return before the packet is relayed successfully or timeouts. let packet_lifetime = interchain.wait_ibc( "akash", transaction_response ).await?; // You can analyze the packet lifetime match packet_lifetime.packets[0].outcome{ IbcPacketOutcome::Success(_) => {}, _ => panic!("Expected packet to be successfully transmitted") }; // You can safely continue with the rest of your application, the packet has been successfully relayed }
This namely removes the need for pausing the program and resuming manually or with a timer. This also allows to automatically get extra information about the relayed packet.
Interchain application testing
Cw-orch allows developers to test their IBC applications and smart-contracts using a common interface. As we know that setting an IBC testing environment is heavy on resources and can be time-consuming, we provide 2 testing environments that will help them streamline their development process:
Rust-only
The MockInterchainEnv
object allows developers to test their application without leaving Rust and without compromising on test speed.
Built on top of cw-multi-test, this environment replicates the actual on-chain IBC module (channel creation as well as packet relaying). This allows you to test any IBC application that leverages Smart-Contract or Bank-module IBC packets. It is really powerful and doesn’t rely on ANY external tools to work. No node setup, no relayer setup, no cluster setup, everything runs inside your crate. Visit the dedicated Mock Interchain Env page for more details and code snippets.
Cosmos SDK Node Testing
The Starship
object allows developers to test their application against the actual binaries of running chains. If you want to run your application with specific logic, custom messages or modules, this is the preferred way. This is the IBC version of the local chains that you can run locally. It can also spin up relayers and set up connections between your local chains automatically.
Visit the dedicated Starship page for more details and code snippets.
Cosmos SDK Node Scripting
The DaemonInterchainEnvironment
object allows developers to script, deploy and manage their application on running chains with attention to IBC functionalities. This enhances the developer experience with more tooling, more useful logging. This is the all-in-one toolbox cor the cosmwasm IBC developer.
Visit the dedicated Daemon Interchain page for more details and code snippets.
Access
The interchain features of cw-orchestrator are not open-source and freely available to all users. Learn more about pricing on our dedicated page.
Interchain Quickstart
General Example
Creating the environment
In order to interact with your environment using IBC capabilities, you first need to create an interchain structure. In this guide, we will create a mock environment for local testing. ↓Click here, if you want to interact with actual nodes.
With mock chains, you can create a mock environment simply by specifying chains ids and sender addresses.
For this guide, we will create 2 chains, juno
and osmosis
, with the same address as sender:
#![allow(unused)] fn main() { let sender = Addr::unchecked("sender_for_all_chains"); let interchain = MockInterchainEnv::new(vec![("juno", &sender), ("osmosis", &sender)]); }
Interacting with the environment
Now, we will work with interchain accounts (ICA). There is a simple implementation of the ICA protocol on Github, and we will use that application with a few simplifications for brevity.
In this protocol, we have 2 smart-contracts that are able to create a connection between them.
The client
will send IBC messages to the host
that in turn will execute the messages on its chain.
Let’s first create the contracts:
#![allow(unused)] fn main() { let juno = interchain.chain("juno")?; let osmosis = interchain.chain("osmosis")?; let client = Client::new("test:client", juno.clone()); let host = Host::new("test:host", osmosis.clone()); client.upload()?; host.upload()?; client.instantiate(&Empty{}, None, None)?; host.instantiate(&Empty{}, None, None)?; }
The Client
and Host
structures here are cw-orchestrator Contracts with registered ibc endpoints.
Client contract definition (Click to get the full code)
#![allow(unused)] fn main() { #[interface( simple_ica_controller::msg::InstantiateMsg, simple_ica_controller::msg::ExecuteMsg, simple_ica_controller::msg::QueryMsg, Empty )] struct Client; impl<Chain: CwEnv> Uploadable for Client<Chain> { // No wasm needed for this example // You would need to get the contract wasm to be able to interact with actual Cosmos SDK nodes fn wasm(&self) -> WasmPath { let wasm_path = format!("No wasm"); WasmPath::new(wasm_path).unwrap() } // Return a CosmWasm contract wrapper with IBC capabilities fn wrapper(&self) -> Box<dyn MockContract<Empty>> { Box::new( ContractWrapper::new_with_empty( simple_ica_controller::contract::execute, simple_ica_controller::contract::instantiate, simple_ica_controller::contract::query, ) .with_ibc( simple_ica_controller::ibc::ibc_channel_open, simple_ica_controller::ibc::ibc_channel_connect, simple_ica_controller::ibc::ibc_channel_close, simple_ica_controller::ibc::ibc_packet_receive, simple_ica_controller::ibc::ibc_packet_ack, simple_ica_controller::ibc::ibc_packet_timeout, ), ) } } }
Host contract definition (Click to get the full code)
#![allow(unused)] fn main() { // This is used because the simple_ica_host contract doesn't have an execute endpoint defined pub fn host_execute(_: DepsMut, _: Env, _: MessageInfo, _: Empty) -> StdResult<Response> { Err(StdError::generic_err("Execute not implemented for host")) } #[interface( simple_ica_host::msg::InstantiateMsg, Empty, simple_ica_host::msg::QueryMsg, Empty )] struct Host; impl<Chain: CwEnv> Uploadable for Host<Chain> { // No wasm needed for this example // You would need to get the contract wasm to be able to interact with actual Cosmos SDK nodes fn wasm(&self) -> WasmPath { let wasm_path = format!("No wasm"); WasmPath::new(wasm_path).unwrap() } // Return a CosmWasm contract wrapper with IBC capabilities fn wrapper(&self) -> Box<dyn MockContract<Empty>> { Box::new( ContractWrapper::new_with_empty( host_execute, simple_ica_host::contract::instantiate, simple_ica_host::contract::query, ) .with_reply(simple_ica_host::contract::reply) .with_ibc( simple_ica_host::contract::ibc_channel_open, simple_ica_host::contract::ibc_channel_connect, simple_ica_host::contract::ibc_channel_close, simple_ica_host::contract::ibc_packet_receive, simple_ica_host::contract::ibc_packet_ack, simple_ica_host::contract::ibc_packet_timeout, ), ) } } }
Then, we can create an IBC channel between the two contracts:
#![allow(unused)] fn main() { let channel_receipt = interchain.create_contract_channel(&client, &host, None, "simple-ica-v2").await?; // After channel creation is complete, we get the channel id, which is necessary for ICA remote execution let juno_channel = channel.0.get_chain("juno")?.channel.unwrap(); }
This step will also await until all the packets sent during channel creation are relayed. In the case of the ICA contracts, a {"who_am_i":{}}
packet is sent out right after channel creation and allows to identify the calling chain.
Finally, the two contracts can interact like so:
#![allow(unused)] fn main() { /// This broadcasts a transaction on the client /// It sends an IBC packet to the host let tx_response = client.send_msgs( juno_channel.to_string(), vec![CosmosMsg::Bank(cosmwasm_std::BankMsg::Burn { amount: vec![cosmwasm_std::coin(100u128, "uosmo")], })], None )?; }
Now, we need to wait for the IBC execution to take place and the relayers to relay the packets. This is done through:
#![allow(unused)] fn main() { let packet_lifetime = interchain.wait_ibc("juno", tx_response).await?; }
After that step, we make sure that the packets were relayed correctly
#![allow(unused)] fn main() { // For testing a successful outcome of the first packet sent out in the tx, you can use: if let IbcPacketOutcome::Success{ ack, .. } = packet_lifetime.packets[0].outcome{ if let IbcPacketAckDecode::Success(_) = ack{ /// Packet has been successfully acknowledged and decoded, the transaction has gone through correctly } /// Else, there was a decode error (maybe you are using the wrong acknowledgement format) }else{ /// Else the packet timed-out, you may have a relayer error or something is wrong in your application }; // OR you can use a helper, that will only error if one of the packets being relayed failed assert_packets_success_decode(packet_lifetime)?; }
If it was relayed correctly, we can proceed with our application.
With this simple guide, you should be able to test and debug your IBC application in no time. Learn more about the implementation and details of the IBC-enabled local testing environment.
With actual Cosmos SDK Nodes
You can also create an interchain environment that interacts with actual running chains. Keep in mind in that case that this type of environment doesn’t allow channel creation. This step will have to be done manually with external tooling. If you’re looking to test your application in a full local test setup, please turn to ↓Starship
#![allow(unused)] fn main() { use cw_orch::prelude::*; use cw_orch::tokio; // This is used to deal with async functions let rt = tokio::runtime::Runtime::new().unwrap(); // We create the daemons ourselves to interact with actual running chains (testnet here) let juno = Daemon::builder() .chain(cw_orch::daemon::networks::UNI_6) .handle(rt.handle()) .build() .unwrap(); // We create the daemons ourselves to interact with actual running chains (testnet here) let osmosis = Daemon::builder() .chain(cw_orch::daemon::networks::OSMO_5) .handle(rt.handle()) .build() .unwrap(); // This will allow us to follow packets execution between juno and osmosis let interchain = DaemonInterchainEnv::from_daemons( vec![juno, osmosis], &ChannelCreationValidator, ); // In case one wants to analyze packets between more chains, you just need to add them to the interchain object }
With this setup, you can now resume this quick-start guide from ↑Interacting with the environment.
You can also learn more about the interchain daemon implementation.
With Starship
You can also create you interchain environment using starship, which allows you to test your application against actual nodes and relayers. This time, an additional setup is necessary. Check out the official Starship Getting Started guide for more details.
Once starship is setup and all the ports forwarded, assuming that starship was run locally, you can execute the following:
#![allow(unused)] fn main() { use cw_orch::prelude::*; use cw_orch::tokio; // This is used to deal with async functions let rt = tokio::runtime::Runtime::new().unwrap(); // This is the starship adapter let starship = Starship::new(rt.handle().to_owned(), None).unwrap(); // You have now an interchain environment that you can use in your application let interchain = starship.interchain_env(); }
This snippet will identify the local Starship setup and initialize all helpers and information needed for interaction using cw-orchestrator. With this setup, you can now resume this quick-start guide from ↑Interacting with the environment
You can also learn more about the interchain daemon implementation.
Execution Environment Integration
Daemon Interchain Environment
This environment allows to interact with actual COSMOS SDK Nodes. Let’s see how that work in details:
Environment creation
For scripting
When scripting with cw-orch-interchain
, developers don’t have to create chain Daemon
objects on their own. You can simply pass chain data to the interchain constructor, and it will create the daemons for you. Like so:
#![allow(unused)] fn main() { use cw_orch::prelude::*; use cw_orch::tokio::runtime::Runtime; use cw_orch::prelude::networks::{LOCAL_JUNO, LOCAL_OSMO}; use cw_orch_interchain::interchain::{ChannelCreationValidator,DaemonInterchainEnv}; let rt = Runtime::new()?; let mut interchain = DaemonInterchainEnv::new(rt.handle(), vec![ (LOCAL_JUNO, None), (LOCAL_OSMO, None) ], &ChannelCreationValidator)?; }
You can then access individual Daemon
objects like so:
#![allow(unused)] fn main() { use cw_orch_interchain::interchain::InterchainEnv; let local_juno: Daemon = interchain.chain("testing")?; let local_osmo: Daemon = interchain.chain("localosmosis")?; }
where the argument of the chain
method is the chain id of the chain you are interacting with. Note that this environment can’t work with chains that have the same chain_id
.
NOTE: Here the
ChannelCreationValidator
struct is a helper that will simply wait for channel creation when it’s called in the script. More information on that channel creation later.
You can also add daemons manually to the interchain
object:
#![allow(unused)] fn main() { let local_migaloo = DaemonBuilder::default() .handle(rt.handle()) .chain(LOCAL_MIGALOO) .build()?; interchain.add_daemons(vec![local_migaloo]); }
For testing
In some cases (we highly recommand it), you might want to interact with local nodes and relayers to test IBC interactions. To do so, we allow users to leverage Starship. Starship is developed by @cosmology_tech and allows developers to spin up a fully simulated mini-cosmos ecosystem. It sets up Cosmos SDK Nodes as well as relayers between them allowing you to focus on your application and less on the testing environment.
For setup, please refer to the official Quick Start. When all that is done, the starship adapter that we provide will detect the deployment and create the right cw-orchestrator
variables and structures for you to interact and test with.
#![allow(unused)] fn main() { use cw_orch_interchain::interchain::{Starship, ChannelCreator}; let rt = Runtime::new()?; let starship = Starship::new(rt.handle().clone(), None)?; let interchain = starship.interchain_env(); let _local_juno: Daemon = interchain.chain("juno-1")?; let _local_osmo: Daemon = interchain.chain("osmosis-1")?; }
NOTE: The second argument of the
Starship::new
function is the optional URL of the starship deployment. It defaults tohttp://localhost:8081
, but you can customize it if it doesn’t match your setup. All the starship data, daemons and relayer setup is loaded from that URL.
General Usage
All interchain environments are centered around the follow_packet
function. In the Daemon case (be it for testing or for scripting), this function is responsible for tracking the relayer interactions associated with the packet lifetime. The lifetime steps of this function are:
- ⬤ On the
source chain
, identify the packet and the destination chain. If the destination chain id is not registered in theinterchain
environment, it will error. Please make sure all the chains your are trying to inspect are included in the environment. - Then, it follows the time line of a packet. A packet can either timeout or be transmitted successfully. The function concurrently does the following steps. If one step returns successfully, the other step will be aborted (as a packet can only have one outcome).
a. Successful cycle:
- ⬤ On the
destination chain
, it looks for the receive transaction of that packet. The function logs the transaction hash as well as the acknowledgement when the receive transaction is found. - ⬤ On the
source chain
, it looks for the acknowledgement transaction of that packet. The function logs when the acknowledgement is received and returns with the transactions involved in the packet broadcast, as well as information about the acknowledgement. b. Timeout: - ⬤ On the
source chain
, it looks for the timeout transaction for that packet. The function logs the transaction hash of the transaction and returns the transaction response corresponding to that transaction.
- ⬤ On the
If you have followed the usage closely, you see that this function doesn’t error when the acknowledgement is an error, has a wrong format or if the packet timeouts. However, the function might error if either of the timeout/successful cycle takes too long. You can customize the wait time in the cw-orchestrator environment variables.
The wait_ibc
function is very similar except that instead of following a single packet, it follows all packets that are being sent within a transaction. This works in a very similar manner and will also not error as long as either a timeout or a successful cycle can be identified before cw-orchestrator
query function timeouts. This function is recursive as it will also look for packets inside the receive/ack/timeout transactions and also follow their IBC cycle. You can think of this function as going down the rabbit-hole of IBC execution and only returning when all IBC interactions are complete.
Analysis Usage
The follow_packet
and wait_ibc
function were coded for scripting usage in mind. They allow to await and repeatedly query Cosmos SDK Nodes until the cycle is complete. However, it is also possible to inspect past transactions using those tools.
Using the DaemonInterchainEnv::wait_ibc_from_txhash
function, one can inspect the history of packets linked to a transaction from a transaction hash only. This enables all kinds of analysis usage, here are some:
- Relayer activity
- Analysis of past transactions for fund recovery
- Whale account analysis
- …
IBC Channel creation
cw-orchestrator doesn’t provide1 relayer capabilities. We only provide tools to analyze IBC activity based on packet relaying mechanism that only relayers can provide. However, when testing your implementation with Starship, you might want to automatically create channels on your test setup.
This is what the second argument of the DaemonInterchainEnv::new
function is used for. You provide an object which will be responsible for creating an IBC channel between two ports. We provide 2 such structures, you can obviously create your own if your needs differ:
-
cw_orch_interchain::interchain::ChannelCreationValidator
This is used when you want to have full control over the channel creation. Wheninterchain.create_channel
is called, the script will stop and prompt you to create a channel with external tools. Once the channel creation process is done on your side, you simply have to input the connection-id on which you created the channel to be able to resume execution. This solution is not ideal at all but allows you to script on actual nodes without having to separate your scripts into multiple parts or change the syntax you coded for your tests.To create the interchain environment with this
ChannelCreator
, use the Validator syntax above. -
cw_orch_interchain::interchain::Starship
This is used when testing your application with Starship. When
interchain.create_channel
is called, the script will simply send a command to the starship cluster to create an IBC channel between the chains that you specified. Obviously, the relayer has to be specified in the starship configuration for this function to return successfully. With this function, you don’t have to worry about anything once your starship cluster is setup properly. The connection-id is returned automatically by the starship library and used throughout after that.To create the interchain environment with this
ChannelCreator
, use the Starship syntax above.
1 as of writing this documentation 09/28/2023
Mock Interchain Environment
This environment allows you to test your IBC application inside your Rust environment without having to create new crates, new packages or even run other programs in the background. This is perfect for iterating through your designs and testing on the go without additional wait time or setup tutorials.
Environment creation
You can create your interchain environment using the following simple setup script:
#![allow(unused)] fn main() { use cw_orch::prelude::*; use cw_orch_interchain::interchain::MockInterchainEnv; let sender = Addr::unchecked("sender"); let mut interchain = MockInterchainEnv::new( vec![("juno-1", &sender), ("osmosis-1", &sender)], ); }
Behind the scenes, Mock
objects are created with the specified chain ids. These mock environments can be used on their own to interact with testing environment directly. You can get those objects like so:
#![allow(unused)] fn main() { use cw_orch_interchain::interchain::InterchainEnv; let local_juno: Daemon = interchain.chain("juno-1")?; let local_osmo: Daemon = interchain.chain("osmosis-1")?; }
where the argument of the chain
method is the chain id of the chain you are interacting with. Note that this environment can’t work with chains that have the same chain_id
.
You can also add mocks manually to the interchain
object, after instantiation:
#![allow(unused)] fn main() { let test_migaloo = Mock::new(&sender); interchain.add_mocks(vec![test_migaloo]); }
General Usage
All interchain environments are centered around the follow_packet
function. In the Mock case, this function is responsible for relaying the packets between the different chains. Using the exact same interface as with other environments, it takes care of all packet relaying procedures.
NOTE: Packets can either go through a successful cycle or timeout. In our current Mock implementation, it’s difficult to do it so that packets timeout. We are working on providing tools for developers to test all edge cases and timeout is part of the helpers we want to bring ASAP.
This function will relay a packets succesfully from the receiving chain back the the source chain. Here is what the full cycle looks like:
- ⬤ On the
source chain
, it queries the packet data associated with the packet channel and sequence. - ⬤ On the
destination chain
, it triggers a receive transaction for that packet. - ⬤ On the
source chain
, it finally triggers an acknowledgement transaction with the data thedestination_chain
returned.
The wait_ibc
function is very similar except that instead of following a single packet, it follows all packets that are being sent within a transaction. This works in a very similar manner and will never return a timeout transaction. This function is recursive as it will also look for packets inside the receive/ack transactions and also follow their IBC cycle. You can think of this function as going down the rabbit-hole of IBC execution and only returning when all IBC interactions are complete.
NOTE: most of the methods on the
interchain
variable presented here are async methods. We recommend reading more about async functions at the point. If you’re not working with any async functions, the gist here is:#![allow(unused)] fn main() { let runtime = tokio::runtime::Runtime::new()?; runtime.block_on( interchain.wait_ibc( &chain_id, tx_response ) ) }
IBC Channel creation
cw-orchestrator also provides tooling for creating channels between mock environments. Here is how you do it:
#![allow(unused)] fn main() { use ibc_relayer_types::core::ics24_host::identifier::PortId; let rt = tokio::runtime::Runtime::new()?; let src_chain = "juno-1".to_string(); let dst_chain = "juno-1".to_string(); let port_id = PortId::transfer(); let (channel, channel_creation_result) = rt.block_on( interchain.create_channel(&src_chain, &dst_chain, None, &port_id, &port_id, "ics20-1") )?; }
- The resulting
channel
object allows you to identify the channel that was just created. It can be useful to retrieve the channel identifiers for instance - The resulting
channel_creation_result
object allows you to identify the different steps of channel creation as well as following all the packets that have been sent during this creation. This is very useful to analyze the effects of the channel creation on external contracts and structures.
Supported Chains
cw-orchestrator currently has support for the chains in the following subsections.
Support a new CosmWasm Chain
If you would like to add support for another chain, please feel free to open a PR!
Issues
Each of the gRPC endpoints has been battle-tested for deployments. If you find any issues, please open an issue!
Archway
Archway is a smart contract platform that directly rewards developers for their contributions. With built-in incentives, Archway equips developers with tools to craft and deploy scalable cross-chain dApps. As these dApps enhance the network’s value, developers enjoy further rewards.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::ARCHWAY_1
.
{{#include ../../../cw-orch/src/daemon/networks/archway.rs:archway}}
References
Injective
Injective is a unique blockchain tailored for finance, offering out-of-the-box modules like a fully decentralized orderbook. As an open smart contracts platform, it hosts a suite of decentralized apps designed for optimal user experience. Dive into Injective and unlock efficient capital allocation in decentralized financial markets.
Usage
To interact with contracts on Injective, first enable the eth
feature for cw-orchestrator. Injective supports EVM-based addresses, and this will enable their use within cw-orchestrator.
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::INJECTIVE_1
.
{{#include ../../../cw-orch/src/daemon/networks/injective.rs:injective}}
References
Juno
Juno stands as the central hub for CosmWasm smart contracts, underpinned by the InterWasm DAO. As a global, open-source, and permission-less platform, Juno champions the forefront of CosmWasm development, enabling developers to seamlessly deploy inter-chain smart contracts crafted in Rust. The network’s inclusive design ensures that anyone can innovate and engage with inter-chain applications.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::JUNO_1
.
{{#include ../../../cw-orch/src/daemon/networks/juno.rs:juno}}
References
Kujira
A decentralized ecosystem for protocols, builders and web3 users seeking sustainable FinTech.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::KAIYO_1
.
{{#include ../../../cw-orch/src/daemon/networks/kujira.rs:kujira}}
References
Neutron
The most secure CosmWasm platform in Cosmos, Neutron lets smart-contracts leverage bleeding-edge Interchain technology with minimal overhead.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::NEUTRON_1
.
{{#include ../../../cw-orch/src/daemon/networks/neutron.rs:neutron}}
References
Nibiru
Nibiru is a breakthrough smart contract platform providing superior throughput, reduced latency, and improved security, all driven by Web Assembly (Wasm) smart contracts. Nibiru simplifies the existing DeFi trading solutions through vertical integration.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::NIBIRU_ITN_2
.
{{#include ../../../cw-orch/src/daemon/networks/nibiru.rs:nibiru}}
References
Osmosis
Osmosis is a cutting-edge decentralized exchange built on the Cosmos network, designed for seamless asset swaps across various interconnected blockchains. As a trailblazer in AMM protocols, Osmosis empowers users with fast, secure, and efficient cross-chain liquidity solutions. The platform’s innovative approach to DeFi positions it as a cornerstone in the Cosmos ecosystem, enabling anyone to effortlessly tap into the vast potential of inter-chain finance.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::OSMO_5
.
{{#include ../../../cw-orch/src/daemon/networks/osmosis.rs:osmosis}}
References
Sei
Sei is the fastest Layer 1 blockchain, designed to scale with the industry.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::SEI_DEVNET_3
.
{{#include ../../../cw-orch/src/daemon/networks/sei.rs:sei}}
References
Terra
Fueled by a passionate community and deep developer talent pool, the Terra blockchain is built to enable the next generation of Web3 products and services.
Usage
See how to setup your main function in the main function section. Update the network passed into the Daemon
builder to be networks::PHOENIX_1
.
{{#include ../../../cw-orch/src/daemon/networks/terra.rs:terra}}
References
Continuous Integration and Deployment
One of the tools that can improve your developer productivity drastically is setting up pipelines for your contract deployments.
cw-orchestrator does not currently add additional support for actions, but an example using the directory structure specified in interfaces can be found below:
# .github/workflows/deploy.yml
---
name: Deploy Contracts
on:
# https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch
workflow_dispatch:
push:
branches: [ 'mainline' ]
paths:
- 'contracts/src/**/*.rs'
- '.github/workflows/deploy.yml'
env:
VERSION_CONTROL_ADDRESS: juno16enwrxhdtsdk8mkkcaj37fgp37wz0r3err4hxfz52lcdyayexnxs4468mu
STATE_FILE: "./state.json" # Optional
ARTIFACTS_DIR: "./target/wasm32-unknown-unknown/release" # Optional
SCHEMA_DIR: "./schema"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown
override: true
- name: Run cargo wasm
uses: actions-rs/cargo@v1
with:
command: build
args: --package counter-app --release --target wasm32-unknown-unknown
env:
RUSTFLAGS: '-C link-arg=-s'
- name: Run deployment script
uses: actions-rs/cargo@v1
with:
command: run
args: --package scripts --bin deploy_app
env:
CHAIN: "juno"
DEPLOYMENT: "debugging"
NETWORK: "local"
RUST_LOG: info
ARTIFACTS_DIR: ${{ env.ARTIFACTS_DIR }}
STATE_FILE: ${{ env.STATE_FILE }}
VERSION_CONTROL_ADDRESS: ${{ env.VERSION_CONTROL_ADDRESS }}
TEST_MNEMONIC: ${{ secrets.TEST_MNEMONIC }}
- name: Upload deployment daemon state
uses: actions/upload-artifact@v2
with:
name: deployment.json
path: ${{ env.STATE_FILE }}
- name: Upload WASM
uses: actions/upload-artifact@v2
with:
# TODO: name env or from cargo
name: counter_app.wasm
path: ${{ env.ARTIFACTS_DIR }}/counter_app.wasm
Contributing to cw-orchestrator
Thank you for considering to contribute to the cw-orchestrator project! We appreciate your support and welcome contributions to help improve this multi-environment CosmWasm smart-contract scripting library. This document provides guidelines and instructions on how to contribute to the project effectively.
Table of Contents
Code of Conduct
By participating in this project, you are expected to uphold our Code of Conduct. Please read the Code of Conduct to ensure that you follow the community guidelines and contribute positively to the project.
Getting Started
To get started with contributing to the cw-orchestrator project, you should first familiarize yourself with the repository structure and the codebase. Please read the project’s README to understand the purpose, features, and usage of the cw-orchestrator library as well as its documentation.
How to Contribute
There are multiple ways to contribute to the cw-orchestrator project, including reporting bugs, suggesting enhancements, and submitting code contributions.
Reporting Bugs
If you encounter any bugs or issues while using the cw-orchestrator library, please report them by creating a new issue in the issue tracker. When reporting a bug, please provide the following information:
- A clear and descriptive title
- A detailed description of the issue, including steps to reproduce it
- Any relevant logs, error messages, or screenshots
- Information about your environment, such as the OS, software versions, and hardware specifications
Suggesting Enhancements
We welcome suggestions for new features or improvements to the existing functionality of the cw-orchestrator library. To suggest an enhancement, create a new issue in the issue tracker with the following information:
- A clear and descriptive title
- A detailed explanation of the proposed enhancement, including its benefits and potential use cases
- If applicable, any examples or mockups of the proposed feature
Code Contributions
To contribute code to the cw-orchestrator project, please follow these steps:
- Fork the repository to your own GitHub account.
- Clone your fork to your local machine.
- Create a new branch for your changes using the
git checkout -b feature/your-feature-name
command. - Make your changes and commit them with a clear and concise commit message.
- Push your branch to your fork on GitHub.
- Create a new pull request against the main branch of the cw-orchestrator repository.
Pull Requests
When submitting a pull request, please make sure that your code follows the Style Guide and that all tests pass. Please provide a detailed description of your changes, including the motivation for the changes and any potential impact on the project. This will help maintainers review your pull request more effectively.
Style Guide
The cw-orchestrator project follows the Rust coding style and conventions. Please ensure that your code adheres to these guidelines to maintain consistency and readability throughout the codebase.
- Use proper indentation (4 spaces) and consistent formatting (
cargo fmt
). - Write descriptive variable and function names.
- Use comments to explain complex or non-obvious code.
- Follow the Rust API Guidelines for API design.
- Add documentation for public functions, types, and modules.
- Write doc tests for public functions and methods.
Community
To join the cw-orchestrator community, please join the Abstract Discord server and the #cw-orchestrator
channel. You can also follow the project on Twitter and GitHub.
References
Easy, right? Try building your contracts with Abstract for the same experience with smart contracts. Get started here.