bolt-cw-sdk 1.0.0

SDK for the BOLT protocol, providing utilities for interacting with the BOLT contracts.
Documentation
use crate::events::{
    get_attr_value_as_coin, get_attr_value_as_str, BOLT_WITHDRAW_BASE_EVENT_NAME,
    BOLT_WITHDRAW_BASE_EVENT_WITHDRAWN_AMOUNT, WASM_EVENT_CONTRACT_ADDRESS,
};
use crate::market::client::MarketAdminClient;
use crate::market::error::MarketError;
use crate::tx_builder::TxBuilder;
use cosmrs::cosmwasm::MsgExecuteContract;
use cosmrs::tendermint::abci::types::ExecTxResult;
use cosmrs::tx::Msg;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use serde::Serialize;

impl MarketAdminClient {
    pub fn append_withdraw_base_msg(
        &self,
        tx_builder: &mut TxBuilder,
        market_address: AccountId,
        receiver: Option<String>,
    ) -> Result<(), MarketError> {
        let receiver = receiver.map(Addr::unchecked);
        let msg = ExecuteMsg {
            withdraw_base: WithdrawBase { 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![],
        };
        let msg = msg.to_any().map_err(MarketError::EyreError)?;
        tx_builder.add_msg(msg);
        Ok(())
    }
}

pub fn parse_withdraw_base_tx_for_bolt_event(
    tx_result: &ExecTxResult,
) -> Result<Vec<WithdrawBaseEvent>, MarketError> {
    let bolt_events = tx_result
        .events
        .iter()
        .filter(|event| event.kind == BOLT_WITHDRAW_BASE_EVENT_NAME)
        .map(|event| {
            let market_address_value = get_attr_value_as_str(event, WASM_EVENT_CONTRACT_ADDRESS)?;
            let withdrawn_amount =
                get_attr_value_as_coin(event, BOLT_WITHDRAW_BASE_EVENT_WITHDRAWN_AMOUNT)?;
            Ok(WithdrawBaseEvent {
                withdrawn_amount,
                market_address: market_address_value.to_string(),
            })
        })
        .collect::<Result<Vec<_>, MarketError>>()?;
    Ok(bolt_events)
}

#[derive(Debug, Clone, PartialEq)]
pub struct WithdrawBaseEvent {
    pub withdrawn_amount: cosmwasm_std::Coin,
    pub market_address: String,
}

#[derive(Serialize)]
struct WithdrawBase {
    pub receiver: Option<Addr>,
}

#[derive(Serialize)]
struct ExecuteMsg {
    pub withdraw_base: WithdrawBase,
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::market::client::MarketAdminClient;
    use crate::oracle::client::OracleAdminClient;
    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, Event, EventAttribute};
    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_deposit_base_msg() {
        let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;

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

        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;

        let protocol_fee_recipient =
            Addr::unchecked("archway1706lywddqxu24ff6426puftkfaw20tyhdkun30pljux7e9alv2fql0q0ca");
        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(
                Addr::unchecked(price_oracle_contract),
                protocol_fee_recipient,
                protocol_fee,
                lp_fee,
                TEST_ASSET_ARCH_SYMBOL.to_owned(),
                vec![TEST_ASSET_USDT_SYMBOL.to_owned()],
                min_base_out,
            )
            .await;

        let market_client = MarketAdminClient::from_scenario(&test_scenario)
            .expect("Failed to create market admin client");

        let admin_account = test_scenario.admin_account();
        test_scenario
            .deposit_base(
                &market_client,
                AccountId::from_str(market_contract_addr.as_ref()).unwrap(),
                &admin_account,
                Coin::new(50, TEST_ASSET_ARCH_SYMBOL).unwrap(),
            )
            .await;

        let account = market_client
            .account(test_scenario.admin_address.clone())
            .await
            .expect("Failed to get account info");
        let mut tx_builder = TxBuilder::new(
            test_scenario.admin_mnemonic.clone(),
            test_scenario.chain_prefix,
            test_scenario.chain_id,
            test_scenario.derivation_path,
            account.sequence,
            account.account_number,
        )
        .expect("Failed to create tx builder");
        market_client
            .append_withdraw_base_msg(
                &mut tx_builder,
                market_contract_addr.clone().parse().unwrap(),
                None,
            )
            .expect("Failed to append withdraw base msg");
        tx_builder.set_memo("From test_append_withdraw_base_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
            .expect("Failed to broadcast tx");

        match response.tx_result.code {
            Code::Ok => {
                println!("Transaction successful: {:?}", response.hash);
                println!("Transaction response: {:?}", response);
                let withdraw_base_event = response
                    .tx_result
                    .events
                    .iter()
                    .find(|ev| ev.kind == "wasm-bolt_withdraw_base")
                    .expect("Failed to find withdraw event");
                assert_event_attribute(
                    withdraw_base_event,
                    "withdrawn_amount",
                    format!("50{}", TEST_ASSET_ARCH_SYMBOL).as_str(),
                );
                assert_event_attribute(withdraw_base_event, "lp", &test_scenario.admin_address);
                assert_event_attribute(withdraw_base_event, "liquidity_amount", "0");
            }
            Code::Err(code) => {
                panic!(
                    "Transaction failed with code: {:?} response: {:?}",
                    code, response
                );
            }
        }
    }

    #[test]
    fn tx_result_no_withdraw_tx_event() {
        let tx_result = super::ExecTxResult {
            code: Code::Ok,
            data: vec![].into(),
            log: String::new(),
            events: vec![],
            gas_wanted: 0,
            gas_used: 0,
            info: String::new(),
            codespace: String::new(),
        };

        let result = super::parse_withdraw_base_tx_for_bolt_event(&tx_result);
        assert!(result.is_ok());
        assert!(result.unwrap().is_empty());
    }

    #[test]
    fn tx_result_multiple_withdraw_tx_events() {
        let tx_result = super::ExecTxResult {
            code: Code::Ok,
            data: vec![].into(),
            log: String::new(),
            events: vec![
                Event {
                    kind: super::BOLT_WITHDRAW_BASE_EVENT_NAME.to_string(),
                    attributes: vec![
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::BOLT_WITHDRAW_BASE_EVENT_WITHDRAWN_AMOUNT.to_string(),
                            value: "500usdt".to_string(),
                            index: false,
                        }),
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::WASM_EVENT_CONTRACT_ADDRESS.to_string(),
                            value: "aaa".to_string(),
                            index: false,
                        }),
                    ],
                },
                Event {
                    kind: "foo".to_string(),
                    attributes: vec![
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::BOLT_WITHDRAW_BASE_EVENT_WITHDRAWN_AMOUNT.to_string(),
                            value: "400eth".to_string(),
                            index: false,
                        }),
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::WASM_EVENT_CONTRACT_ADDRESS.to_string(),
                            value: "bbb".to_string(),
                            index: false,
                        }),
                    ],
                },
                Event {
                    kind: super::BOLT_WITHDRAW_BASE_EVENT_NAME.to_string(),
                    attributes: vec![
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::BOLT_WITHDRAW_BASE_EVENT_WITHDRAWN_AMOUNT.to_string(),
                            value: "300arch".to_string(),
                            index: false,
                        }),
                        EventAttribute::V037(cosmrs::tendermint::abci::v0_37::EventAttribute {
                            key: super::WASM_EVENT_CONTRACT_ADDRESS.to_string(),
                            value: "ccc".to_string(),
                            index: false,
                        }),
                    ],
                },
            ],
            gas_wanted: 0,
            gas_used: 0,
            info: String::new(),
            codespace: String::new(),
        };

        let result = super::parse_withdraw_base_tx_for_bolt_event(&tx_result)
            .expect("Failed to parse events");

        assert_eq!(
            result,
            [
                WithdrawBaseEvent {
                    withdrawn_amount: cosmwasm_std::coin(500, "usdt"),
                    market_address: "aaa".to_string(),
                },
                WithdrawBaseEvent {
                    withdrawn_amount: cosmwasm_std::coin(300, "arch"),
                    market_address: "ccc".to_string(),
                }
            ]
        );
    }
}