bolt-cw-sdk 1.0.0

SDK for the BOLT protocol, providing utilities for interacting with the BOLT contracts.
Documentation
use super::account::TestAccount;
use crate::chain_config::ChainConfig;
use crate::cosmos_client::gas_info::ClientGasInfo;
use crate::cosmos_client::CosmosClient;
use crate::tx_builder::derivation_path::CosmosDerivationPath;
use crate::tx_builder::TxBuilder;
use cosmrs::bank::MsgSend;
use cosmrs::cosmwasm::{MsgExecuteContract, MsgInstantiateContract};
use cosmrs::tendermint::abci::Code;
use cosmrs::tx::Msg;
use cosmrs::{AccountId, Coin};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::SystemTime;
use tendermint_rpc::{HttpClient, WebSocketClient};

static CONTRACT_COUNTER: AtomicUsize = AtomicUsize::new(0);

pub struct TestScenario {
    pub cosmos_client: Arc<CosmosClient>,
    pub websocket_client: Arc<CosmosClient<WebSocketClient>>,
    pub admin_mnemonic: String,
    pub chain_prefix: String,
    pub chain_id: String,
    pub chain_denom: String,
    pub derivation_path: CosmosDerivationPath,
    pub admin_address: String,
    pub price_feeder_account: TestAccount,
    pub gas_price: u128,
    pub gas_adjustment: Option<f64>,

    pub oracle_code_id: u64,
    pub settlement_code_id: u64,
    pub router_code_id: u64,
}

impl TestScenario {
    #[allow(clippy::too_many_arguments)]
    pub async fn new(
        rpc_url: String,
        websocket_url: String,
        admin_mnemonic: String,
        chain_prefix: String,
        chain_id: String,
        chain_denom: String,
        derivation_path: CosmosDerivationPath,
        gas_price: u128,
        gas_adjustment: Option<f64>,
        oracle_code_id: u64,
        settlement_code_id: u64,
        router_code_id: u64,
    ) -> Self {
        let tx_builder = TxBuilder::new(
            admin_mnemonic.clone(),
            chain_prefix.clone(),
            chain_id.clone(),
            derivation_path,
            0,
            0,
        )
        .expect("Failed to create TxBuilder");
        let admin_address = tx_builder.account_id.to_string();

        let cosmos_client = Arc::new(
            CosmosClient::<HttpClient>::new(&rpc_url)
                .await
                .unwrap()
                .with_gas_info(
                    ClientGasInfo::new(gas_price, chain_denom.clone())
                        .with_adjustment(gas_adjustment.unwrap_or(1.0)),
                ),
        );
        let websocket_client = Arc::new(
            CosmosClient::<WebSocketClient>::new(&websocket_url)
                .await
                .unwrap(),
        );

        let price_feeder_account =
            TestAccount::new_random(&chain_prefix, &chain_id, derivation_path);
        let scenario = TestScenario {
            cosmos_client,
            websocket_client,
            admin_mnemonic,
            chain_prefix,
            chain_id,
            chain_denom,
            derivation_path,
            admin_address,
            price_feeder_account,
            gas_price,
            gas_adjustment,
            oracle_code_id,
            settlement_code_id,
            router_code_id,
        };

        scenario
            .fund_account(&scenario.price_feeder_account, 1)
            .await;

        scenario
    }

    pub fn gas_info(&self) -> ClientGasInfo {
        ClientGasInfo::new(self.gas_price, self.chain_denom.clone())
            .with_adjustment(self.gas_adjustment.unwrap_or(1.0))
    }

    pub async fn new_from_config(file_path: String) -> Self {
        let config = std::fs::read_to_string(file_path).expect("Failed to read config file");
        let config: serde_json::Value =
            serde_json::from_str(&config).expect("Failed to parse config file");
        let rpc_url = config["rpc_client_url"]
            .as_str()
            .expect("Failed to get rpc url")
            .to_string();
        let websocket_url = config["websocket_url"]
            .as_str()
            .expect("Failed to get websocket url")
            .to_string();
        let admin_mnemonic = config["admin_mnemonic"]
            .as_str()
            .expect("Failed to get admin mnemonic")
            .to_string();
        let chain_prefix = config["chain_prefix"]
            .as_str()
            .expect("Failed to get chain prefix")
            .to_string();
        let chain_id = config["chain_id"]
            .as_str()
            .expect("Failed to get chain id")
            .to_string();
        let chain_denom = config["chain_denom"]
            .as_str()
            .expect("Failed to get chain denom")
            .to_string();
        let derivation_path = CosmosDerivationPath::default();
        let oracle_code_id = config["oracle_code_id"]
            .as_u64()
            .expect("Failed to get oracle code id");
        let settlement_code_id = config["settlement_code_id"]
            .as_u64()
            .expect("Failed to get settlement code id");
        let router_code_id = config["router_code_id"]
            .as_u64()
            .expect("Failed to get router code id");
        let gas_price = config["gas_price"]
            .as_u64()
            .expect("Failed to get gas price") as u128;
        let gas_adjustment = config["gas_adjustment"].as_f64();

        TestScenario::new(
            rpc_url,
            websocket_url,
            admin_mnemonic,
            chain_prefix,
            chain_id,
            chain_denom,
            derivation_path,
            gas_price,
            gas_adjustment,
            oracle_code_id,
            settlement_code_id,
            router_code_id,
        )
        .await
    }

    pub fn get_chain_config(&self, oracle_contract_address: &str) -> ChainConfig {
        let oracle_contract_address = oracle_contract_address
            .parse()
            .expect("Failed to parse oracle contract address");
        ChainConfig {
            prefix: self.chain_prefix.clone(),
            oracle_contract_address,
            chain_id: self.chain_id.clone(),
            fee_denom: self.chain_denom.clone(),
        }
    }

    pub fn add_contract_instantiate_msg(
        &self,
        tx_builder: &mut TxBuilder,
        code_id: u64,
        label: String,
        msg: Vec<u8>,
        funds: Vec<Coin>,
    ) {
        let msg = MsgInstantiateContract {
            sender: tx_builder.account_id.clone(),
            code_id,
            label: Some(label),
            msg,
            funds,
            admin: Some(tx_builder.account_id.clone()),
        };
        let any_msg = msg
            .to_any()
            .expect("Failed to create contract instantiate msg");
        tx_builder.add_msg(any_msg);
    }

    pub fn add_contract_execute_msg(
        &self,
        tx_builder: &mut TxBuilder,
        contract_address: AccountId,
        msg: Vec<u8>,
        funds: Vec<Coin>,
    ) {
        let msg = MsgExecuteContract {
            sender: tx_builder.account_id.clone(),
            contract: contract_address,
            msg,
            funds,
        };
        let any_msg = msg.to_any().expect("Failed to create contract execute msg");
        tx_builder.add_msg(any_msg);
    }

    pub async fn instantiate_contract(
        &self,
        msg: Vec<u8>,
        code_id: u64,
        contract_name: &str,
    ) -> String {
        let account = self
            .cosmos_client
            .account(self.admin_address.clone())
            .await
            .expect("Get account");
        let mut tx_builder = TxBuilder::new(
            self.admin_mnemonic.clone(),
            self.chain_prefix.clone(),
            self.chain_id.clone(),
            self.derivation_path,
            account.sequence,
            account.account_number,
        )
        .expect("Create TxBuilder");

        self.add_contract_instantiate_msg(
            &mut tx_builder,
            code_id,
            format!("bolt-{}", contract_name),
            msg,
            vec![],
        );
        tx_builder.set_memo(format!(
            "Instantiate {}-{} contract at {}",
            contract_name,
            CONTRACT_COUNTER.fetch_add(1, Ordering::Relaxed),
            SystemTime::now()
                .duration_since(SystemTime::UNIX_EPOCH)
                .unwrap()
                .as_nanos()
        ));
        tx_builder.set_timeout_height(0u8);
        let gas = 480_000u64;
        let amount = 65_000_000_000_000_000u128;
        tx_builder.set_fee(amount, &self.chain_denom, gas);
        let signed_bytes = tx_builder.get_signed_bytes().expect("Get signed bytes");
        let response = self
            .cosmos_client
            .broadcast_tx(signed_bytes)
            .await
            .expect("Should broadcast tx");
        match response.tx_result.code {
            Code::Ok => {
                if response.check_tx.code.is_err() {
                    panic!(
                        "Failed to send the tx.\nCode: {}\nLog: {}",
                        response.check_tx.code.value(),
                        response.check_tx.log
                    );
                }
                println!(
                    "{} contract instantiated with hash: {:?}",
                    contract_name,
                    response.hash.to_string()
                );

                let instantiate_event = response
                    .tx_result
                    .events
                    .iter()
                    .find(|ev| ev.kind == "instantiate")
                    .expect("Should find instantiate event");
                let contract_address_event_attr = instantiate_event
                    .attributes
                    .iter()
                    .find(|attr| {
                        attr.key_str().expect("Should stringify attribute key")
                            == "_contract_address"
                    })
                    .expect("Should find the contract address attribute")
                    .clone();
                contract_address_event_attr
                    .value_str()
                    .expect("Should stringify the contract address")
                    .to_string()
            }
            _ => {
                panic!(
                    "Failed to instantiate {} contract: {:?}",
                    contract_name, response
                );
            }
        }
    }

    pub fn add_send_msg(
        &self,
        tx_builder: &mut TxBuilder,
        sender: AccountId,
        receiver: AccountId,
        denom: &str,
        amount: u128,
    ) {
        let coin = Coin {
            denom: denom.parse().unwrap(),
            amount,
        };

        let send_msg = MsgSend {
            from_address: sender,
            to_address: receiver,
            amount: vec![coin],
        };
        let msg = send_msg.to_any().expect("Should serialize the SendMsg");
        tx_builder.add_msg(msg);
    }

    pub fn admin_account(&self) -> TestAccount {
        TestAccount {
            mnemonic: self.admin_mnemonic.clone(),
            account_id: self.admin_address.parse().unwrap(),
        }
    }
}