Deploying and scripting your smart contract using BOOT

Abstract Contact
Abstract Money
Published in
6 min readNov 28, 2022

Once you have written your CosmWasm smart contract, you are probably ready to start writing integration tests or deploy it to a live network. Doing this through frontend interfaces such as CosmWasm Tools can work, but is certainly not a good long term solution. Executing this through the CLI is an even more time-consuming and menial process.

BOOT (by Abstract) simplifies all of this by providing a type-safe library and a standard way of deploying and interacting with your CosmWasm smart contracts.

Getting Started

The following sections detail setting up a library for interfaces and a separate package for the scriptsso that publishing the contract interfaces is easy.

Contract Interface Library

To get started with BOOT, create a new folder anywhere (though we often do it alongside our contracts in the same repository) called interfaces by running:

cargo init --lib interfaces

Following this example, the file structure will now look like:

.
├── Cargo.toml
├── contracts
│ ├── Cargo.toml
│ └── src
│ ├── contract.rs
│ └── ...
└── interfaces
├── Cargo.toml
└── src
└── lib.rs

If your cargo project is a workspace, be sure to add interfaces to the [workspace].members array at the workspace root.

Install boot-core

cd interfaces
cargo add boot-core
cargo add --path ../contracts

Then add any other dependencies, which should look similar to the following:

[dependencies]
boot-core = "0.1.3" # latest version as of writing this article
my-contract = { path = "../contracts" }
# common
cosmwasm-std = "1.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }

Defining Contract Interfaces

The contract interface is what your scripts will be interacting with to deploy and manage the deployed instance of your contract. It provides accessible methods for interfacing with the contract entry points and can maintain state between script runs based on the deployment configuration.

First, create a new file in the src directory of the interfaces package, and add it to the library declaration file:

touch src/my-contract.rs
echo 'pub mod my_contract;' >> src/lib.rs

In your new contract file, define a struct for your contract interface and provide the [Instantiate|Execute|Query|Migrate]Msgs to the boot_contractmacro, which will generate fully-typed instantiate, execute, query, and migrate methods.

use boot_core::prelude::boot_contract;
use my_contract::{InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg};

#[boot_contract(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)]
pub struct MyContract<Chain>;

The generic “<Chain>” argument is to help the rust-analyzer with type analysis because sometimes the macro-expansion engine does not fully understand generically typed macros.

If your entry point Msgs have any generic arguments, pull them out into newtypes before passing into the macro.

Next, you’ll want to define the constructor for the struct defined above. In the following example:

use boot_core::prelude::{BootEnvironment, Contract};

// ...

impl<Chain: BootEnvironment> MyContract<Chain> {
/// Construct a new instance of MyContract
/// * `contract_id` - what your contract should be called in local state (*not* on-chain)
/// * `chain` - the chain to deploy to
pub fn new(contract_id: &str, chain: &Chain) -> Self {
let crate_path = env!("CARGO_MANIFEST_DIR");
// This can be the absolute path of the file like it is below
// or it can simply be the name of the contract file like "my-contract",
// and the path of that file set via WASM_DIR env variable
let wasm_path = "../../target/wasm32-unknown-unknown/release/my-contract.wasm";
Self(
Contract::new(contract_id, chain)
.with_wasm_path(wasm_path),
)
}
}

Notice that we build the Contract instance with_wasm_path, where we provide the contract name. This contract name will be used to search the artifacts directory (set by ARTIFACTS_DIR env variable) for the .wasm file of your contract. Alternatively you can specify a path to the released wasm after running RUSTFLAGS=’-C link-arg=-s’ cargo wasm . See the CosmWasm documentation on compiling your contract for more information.

Script

Now that we have the interface written for our contract, we can start writing scripts to deploy and interact with it.

Setup

Like before, we’re going to setup a new folder for our scripts. This time, we’ll call it scripts and initialize it as a binary crate:

cargo init --bin scripts

If your cargo project is a workspace, be sure to add scripts to the [workspace].members array at the workspace root.

Your scripts will have basically the same dependencies as your contract interfaces, but with a few additions:

cargo add --path ../interfaces

and also add the anyhow and dotenv crates:

cargo add anyhow dotenv log

Env Configuration

The dotenv crate will allow us to load environment variables from a .env file. This is useful for setting up the chain configuration for your scripts.

# .env
# info, debug, trace
RUST_LOG=info

# where the contract wasms are located
ARTIFACTS_DIR="../artifacts"

# where to store the output state data
DAEMON_STATE_PATH="./daemon_state.json"

# Mnemonics of the account that will be used to sign transactions
LOCAL_MNEMONIC=""
TEST_MNEMONIC=""
MAIN_MNEMONIC=""

IMPORTANT: Make sure to exclude the .env file in your gitignore.

Main Function

Now that we have our dependencies setup, we can start writing our script. Either create a new file in the src directory of the scripts/src package, or use the main.rs file that was created by default.

This function is mostly just boilerplate, so you can copy and paste it into your new script file. It will just call your function and give you nicer error traces:

fn main() {
dotenv().ok();
env_logger::init();

use dotenv::dotenv;
if let Err(ref err) = deploy_contract() {
log::error!("{}", err);
err.chain()
.skip(1)
.for_each(|cause| log::error!("because: {}", cause));
::std::process::exit(1);
}
}

Deployment Function

First, we’ll define a function that will deploy our contract to the chain. This function will setup the environment (connecting to the chain), deploy the contract, and return a Result with the contract address.

// scripts/src/my_contract.rs
use anyhow::Result;
use boot_core::networks;
use boot_core::prelude::{instantiate_daemon_env, NetworkInfo};
// Traits for contract deployment
use boot_core::interface::*;
use interfaces::my_contract::MyContract;

// Select the chain to deploy to
const NETWORK: NetworkInfo = networks::juno::UNI_5;
const CONTRACT_NAME: &str = "my-contract";

pub fn deploy_contract() -> anyhow::Result<String> {
// Setup the environment
let (_, _sender, chain) = instantiate_daemon_env(network)?;

// Create a new instance of your contract interface
let contract = MyContract::new(CONTRACT_NAME, &chain);
// Upload your contract
contract.upload()?;

// Instantiate your contract
let init_msg = InstantiateMsg {
// ...
};
// The second argument is the admin, the third is any coins to send with the init message
contract.instantiate(init_msg, None, None)?;

// Load and return the contract address
let contract_addr = contract.address()?;
Ok(contract_addr)
}

Additional Scripts

So you have your contract deployed, but what now? You can write additional scripts to interact with your contract. For example, you can write a script to query the contract state, or to execute a contract method.

Here’s an example of a script that queries the contract state:

// scripts/src/my_contract.rs
// use ...
use my_contract::{QueryMsg};
// ...

pub fn query_contract() -> Result<()> {
// Setup the environment
let (_, _sender, chain) = instantiate_daemon_env(NETWORK)?;

let contract = MyContract::new(CONTRACT_NAME, &chain);
// Load the contract address (this will use the address set from the previous deploy script)
let contract_addr = contract.address();
// Query the contract
let res = contract.query(QueryMsg::Balance {
address: contract_addr,
})?;
// Print the result
println!("{:?}", res);
Ok(())
}

And one that executes a contract method:

// scripts/src/my_contract.rs
// use ...
use my_contract::{ExecuteMsg};
// ...

pub fn execute_contract() -> Result<()> {
// Setup the environment
let (_, _sender, chain) = instantiate_daemon_env(NETWORK)?;


let contract = MyContract::new(CONTRACT_NAME, &chain);
let contract_addr = contract.address();

// Execute a contract method
let res = contract.execute(ExecuteMsg::UpdateBalance {
address: contract_addr,
balance: Uint128::from(1000000u128),
})?;
// Print the result
println!("{:?}", res);
Ok(())
}

Refinement

You can also refine your contract interface to make it easier to use. For example, you can create a function that will execute a specific contract method and return the result, instead of having to call contract.execute and contract.query separately.

// interfaces/src/my_contract.rs
// Import the boot traits
use boot_core::interface::*;
// ...


impl<Chain: BootEnvironment> MyContract<Chain> {
pub fn new(contract_id: &str, chain: &Chain) -> Self {
// ...
}

/// Query the balance of an address
/// `address` - the address to query
pub fn balance(&self, address: Addr) -> Result<BalanceResponse> {
let balance_query = QueryMsg::Balance { address };
self.query(balance_query)
}

/// Update the balance of an address
/// `address` - the address to update
/// `balance` - the new balance
pub fn update_balance(&self, address: Addr, balance: Uint128) -> Result<ExecuteResult> {
let update_balance_msg = ExecuteMsg::UpdateBalance {
address,
balance,
};
self.execute(update_balance_msg)
}
}

References

--

--