bolt-cw-sdk 1.0.0

SDK for the BOLT protocol, providing utilities for interacting with the BOLT contracts.
Documentation
use crate::market::client::MarketAdminClient;
use crate::market::error::MarketError;
use crate::tx_builder::TxBuilder;
use cosmrs::cosmwasm::MsgExecuteContract;
use cosmrs::tx::Msg;
use cosmrs::{AccountId, Coin};
use cosmwasm_std::{Addr, Uint128};
use serde::Serialize;

impl MarketAdminClient {
    pub fn append_swap_msg(
        &self,
        tx_builder: &mut TxBuilder,
        market_address: AccountId,
        swap_amount: Coin,
        minimum_out: Option<Uint128>,
        receiver: Option<Addr>,
    ) -> Result<(), MarketError> {
        let msg = ExecuteMsg {
            swap: SwapMsg {
                minimum_out,
                receiver,
            },
        };
        let contract_msg = serde_json::to_vec(&msg).map_err(MarketError::SerdeJsonError)?;
        let msg = MsgExecuteContract {
            sender: tx_builder.account_id.clone(),
            contract: market_address.clone(),
            msg: contract_msg,
            funds: vec![swap_amount],
        };
        let msg = msg.to_any().map_err(MarketError::EyreError)?;
        tx_builder.add_msg(msg);
        Ok(())
    }
}

#[derive(Serialize)]
struct SwapMsg {
    /// The minimum amount of base asset user wants to receive
    pub minimum_out: Option<Uint128>,
    /// The address to send the base asset to
    pub receiver: Option<Addr>,
}

#[derive(Serialize)]
struct ExecuteMsg {
    pub swap: SwapMsg,
}

#[cfg(test)]
mod tests {
    use crate::cosmos_client::error::CosmosClientError;
    use crate::events::{
        BOLT_SWAP_EVENT_AMOUNT_IN, BOLT_SWAP_EVENT_AMOUNT_OUT, BOLT_SWAP_EVENT_DENOM_IN,
        BOLT_SWAP_EVENT_DENOM_OUT, BOLT_SWAP_EVENT_LP_FEE_AMOUNT,
        BOLT_SWAP_EVENT_PROTOCOL_FEE_AMOUNT, BOLT_SWAP_EVENT_SENDER,
    };
    use crate::market::client::MarketAdminClient;
    use crate::market::error::MarketError;
    use crate::oracle::client::OracleAdminClient;
    use crate::test_utils::account::TestAccount;
    use crate::test_utils::helpers::{
        assert_event_attribute, TEST_ASSET_ARCH_SYMBOL, TEST_ASSET_USDT_SYMBOL,
    };
    use crate::test_utils::test_scenario::TestScenario;
    use crate::tx_builder::TxBuilder;
    use cosmrs::tendermint::abci::Code;
    use cosmrs::{AccountId, Coin};
    use cosmwasm_std::{Addr, Decimal256, Uint128};
    use serial_test::serial;
    use std::ops::Add;
    use std::str::FromStr;
    use std::time::{Duration, SystemTime, UNIX_EPOCH};

    #[tokio::test]
    #[serial]
    async fn test_append_swap_msg() {
        let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;

        // Initialize oracle contract
        let price_threshold_ratio = Decimal256::from_str("0.1").unwrap();
        let price_expire_millis = Some(1000);
        let oracle_contract_address = test_scenario
            .instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
            .await;
        let client = OracleAdminClient::from_scenario(&test_scenario, &oracle_contract_address)
            .expect("Failed to create oracle admin client");

        // Set default assets and prices
        test_scenario.set_default_assets(&client).await;
        let price_expiry_time = SystemTime::now().add(Duration::from_secs(7200)); // 2h from now
        let price_expiry_timestamp = price_expiry_time
            .duration_since(UNIX_EPOCH)
            .expect("Converting to timestamp failed");
        test_scenario
            .set_default_prices(&client, price_expiry_timestamp, "50000")
            .await;

        // Instantiate market contract
        let new_account = test_scenario.create_new_account().await;
        let protocol_fee = Decimal256::percent(10);
        let lp_fee = Decimal256::percent(10);
        let min_base_out = Uint128::new(10);

        let market_contract_addr = test_scenario
            .instantiate_settlement_contract(
                oracle_contract_address,
                new_account.account_id.to_string(),
                protocol_fee,
                lp_fee,
                TEST_ASSET_ARCH_SYMBOL.to_owned(),
                vec![TEST_ASSET_USDT_SYMBOL.to_owned()],
                min_base_out,
            )
            .await;

        // Deposit base assets
        let market_client = MarketAdminClient::from_scenario(&test_scenario).unwrap();
        let admin_account = test_scenario.admin_account();
        test_scenario
            .deposit_base(
                &market_client,
                AccountId::from_str(&market_contract_addr).unwrap(),
                &admin_account,
                Coin::new(10000000, TEST_ASSET_ARCH_SYMBOL).unwrap(),
            )
            .await;

        // Swap quote with base
        let market_client = MarketAdminClient::from_scenario(&test_scenario)
            .expect("Failed to create market admin client");
        let account = market_client
            .account(new_account.account_id.to_string())
            .await
            .expect("Failed to get account info");
        let mut tx_builder = TxBuilder::new(
            new_account.mnemonic,
            test_scenario.chain_prefix.clone(),
            test_scenario.chain_id.clone(),
            test_scenario.derivation_path,
            account.sequence,
            account.account_number,
        )
        .expect("Failed to create tx builder");

        let transfer_amount = 5_000_000;
        let minimum_base_out = Some(Uint128::new(10));
        let receiver = Some(Addr::unchecked(new_account.account_id.clone()));
        let market_contract_account_id = market_contract_addr.to_string().parse().unwrap();
        let coin = Coin::new(transfer_amount, TEST_ASSET_USDT_SYMBOL).unwrap();

        market_client
            .append_swap_msg(
                &mut tx_builder,
                market_contract_account_id,
                coin,
                minimum_base_out,
                receiver,
            )
            .expect("Failed to append swap msg");
        tx_builder.set_memo("From test_append_swap_msg".to_string());
        let gas = 410_000u64;
        let amount = 70_000_000_000_000_000u128;
        tx_builder.set_fee(amount, &test_scenario.chain_denom, gas);
        let signed_bytes = tx_builder
            .get_signed_bytes()
            .expect("Failed to get signed bytes");
        let response = market_client
            .broadcast_tx(signed_bytes)
            .await
            .expect("Failed to broadcast tx");

        match response.tx_result.code {
            Code::Ok => {
                println!("Transaction successful: {:?}", response.hash);
                println!("Transaction response: {:?}", response);

                let update_config_event = response
                    .tx_result
                    .events
                    .iter()
                    .find(|ev| ev.kind == "wasm-bolt_swap")
                    .expect("Failed to find update config event");
                assert_event_attribute(
                    update_config_event,
                    BOLT_SWAP_EVENT_PROTOCOL_FEE_AMOUNT,
                    "500000",
                );
                assert_event_attribute(update_config_event, BOLT_SWAP_EVENT_LP_FEE_AMOUNT, "9");
                assert_event_attribute(
                    update_config_event,
                    BOLT_SWAP_EVENT_AMOUNT_IN,
                    &transfer_amount.to_string(),
                );
                assert_event_attribute(update_config_event, BOLT_SWAP_EVENT_AMOUNT_OUT, "81");
                assert_event_attribute(
                    update_config_event,
                    BOLT_SWAP_EVENT_SENDER,
                    new_account.account_id.as_ref(),
                );
                assert_event_attribute(
                    update_config_event,
                    BOLT_SWAP_EVENT_DENOM_IN,
                    TEST_ASSET_USDT_SYMBOL,
                );
                assert_event_attribute(
                    update_config_event,
                    BOLT_SWAP_EVENT_DENOM_OUT,
                    TEST_ASSET_ARCH_SYMBOL,
                );
            }
            Code::Err(code) => {
                panic!(
                    "Transaction failed with code: {:?} response: {:?}",
                    code, response
                );
            }
        }
    }

    #[tokio::test]
    #[serial]
    async fn not_enough_of_an_asset() {
        let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
        let new_account = test_scenario.create_new_account_with_funds(1_000_000).await;

        let market_contract_addr = init(&mut test_scenario, &new_account).await;

        let market_client = MarketAdminClient::from_scenario(&test_scenario)
            .expect("Failed to create market admin client");
        let account = market_client
            .account(new_account.account_id.to_string())
            .await
            .expect("Failed to get account info");
        let mut tx_builder = TxBuilder::new(
            new_account.mnemonic,
            test_scenario.chain_prefix.clone(),
            test_scenario.chain_id.clone(),
            test_scenario.derivation_path,
            account.sequence,
            account.account_number,
        )
        .expect("Failed to create tx builder");

        let transfer_amount = 5_000_000;
        let minimum_base_out = Some(Uint128::new(10));
        let receiver = Some(Addr::unchecked(new_account.account_id.clone()));
        let market_contract_account_id = market_contract_addr.to_string().parse().unwrap();
        let coin = Coin::new(transfer_amount, TEST_ASSET_USDT_SYMBOL).unwrap();

        market_client
            .append_swap_msg(
                &mut tx_builder,
                market_contract_account_id,
                coin,
                minimum_base_out,
                receiver,
            )
            .expect("Failed to append swap msg");
        tx_builder.set_memo("From test_append_swap_msg".to_string());
        let gas = 400_000u64;
        let amount = 70_000_000_000_000_000u128;
        tx_builder.set_fee(amount, &test_scenario.chain_denom, gas);
        let signed_bytes = tx_builder
            .get_signed_bytes()
            .expect("Failed to get signed bytes");
        let response = market_client.broadcast_tx(signed_bytes).await;

        match response {
            Err(MarketError::CosmosClientError(CosmosClientError::BroadcastTx(err))) => assert_eq!(err, "failed to execute message; message index: 0: spendable balance 1000000uusd is smaller than 5000000uusd: insufficient funds"),
            Err(err) => panic!("Unexpected error: {:?}", err),
            Ok(_) => panic!("Expected an error, but got a successful response"),
        }
    }

    async fn init(test_scenario: &mut TestScenario, new_account: &TestAccount) -> String {
        // Initialize oracle contract
        let price_threshold_ratio = Decimal256::from_str("0.1").unwrap();
        let price_expire_millis = Some(1000);
        let oracle_contract_address = test_scenario
            .instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
            .await;
        let client = OracleAdminClient::from_scenario(test_scenario, &oracle_contract_address)
            .expect("Failed to create oracle admin client");

        // Set default assets and prices
        test_scenario.set_default_assets(&client).await;
        let price_expiry_time = SystemTime::now().add(Duration::from_secs(7200)); // 2h from now
        let price_expiry_timestamp = price_expiry_time
            .duration_since(UNIX_EPOCH)
            .expect("Converting to timestamp failed");
        test_scenario
            .set_default_prices(&client, price_expiry_timestamp, "50000")
            .await;

        // Instantiate market contract
        let protocol_fee = Decimal256::percent(10);
        let lp_fee = Decimal256::percent(10);
        let min_base_out = Uint128::from(1000u64);

        let market_contract_addr = test_scenario
            .instantiate_settlement_contract(
                oracle_contract_address,
                new_account.account_id.to_string(),
                protocol_fee,
                lp_fee,
                TEST_ASSET_ARCH_SYMBOL.to_owned(),
                vec![TEST_ASSET_USDT_SYMBOL.to_owned()],
                min_base_out,
            )
            .await;

        // Deposit base assets
        let market_client = MarketAdminClient::from_scenario(test_scenario).unwrap();
        let admin_account = test_scenario.admin_account();
        test_scenario
            .deposit_base(
                &market_client,
                AccountId::from_str(&market_contract_addr).unwrap(),
                &admin_account,
                Coin::new(10000000, TEST_ASSET_ARCH_SYMBOL).unwrap(),
            )
            .await;

        market_contract_addr
    }
}