use crate::events::{
get_attr_value_as_str, get_attr_value_as_u128, BOLT_SWAP_EVENT_AMOUNT_IN,
BOLT_SWAP_EVENT_AMOUNT_OUT, BOLT_SWAP_EVENT_CONTRACT_ADDRESS, BOLT_SWAP_EVENT_DENOM_IN,
BOLT_SWAP_EVENT_DENOM_OUT, BOLT_SWAP_EVENT_LP_FEE_AMOUNT, BOLT_SWAP_EVENT_NAME,
BOLT_SWAP_EVENT_PRICE, BOLT_SWAP_EVENT_PROTOCOL_FEE_AMOUNT, BOLT_SWAP_EVENT_SENDER,
};
use crate::router::client::RouterClient;
use crate::router::error::RouterError;
use crate::timestamp::Timestamp;
use cosmrs::tendermint::abci::types::ExecTxResult;
use cosmrs::tendermint::Time;
use cosmwasm_std::Decimal256;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
impl RouterClient {
pub async fn get_trade(&self, tx_hash: String) -> Result<TradeResponse, RouterError> {
let tx_response = self.get_tx(tx_hash).await?;
let block = self.cosmos_client.block(tx_response.height.value()).await?;
parse_tx_for_bolt_event(
&tx_response.tx_result,
block.block.header.height.value(),
Some(block.block.header.time),
)
}
pub async fn get_trades_in_block(
&self,
height: u64,
) -> Result<Vec<TradeResponse>, RouterError> {
let block_res = self.cosmos_client.block_results(height).await?;
let mut trades: Vec<TradeResponse> = match block_res.txs_results {
Some(txs_results) if !txs_results.is_empty() => txs_results
.into_iter()
.filter_map(|tx| parse_tx_for_bolt_event(&tx, block_res.height.value(), None).ok())
.collect(),
_ => Vec::new(),
};
for trade in trades.iter_mut() {
if trade.timestamp.is_none() {
let height = trade.trade_block_height;
let block = self.cosmos_client.block(height).await?;
trade.timestamp = Some(block.block.header.time.into());
}
}
Ok(trades)
}
}
pub fn parse_tx_for_bolt_event(
tx_result: &ExecTxResult,
block_height: u64,
block_time: Option<Time>,
) -> Result<TradeResponse, RouterError> {
let bolt_event = tx_result
.events
.iter()
.find(|event| event.kind == BOLT_SWAP_EVENT_NAME)
.ok_or(RouterError::NotBoltTransactionError)?;
let amount_in_value = get_attr_value_as_u128(bolt_event, BOLT_SWAP_EVENT_AMOUNT_IN)?;
let denom_in_value = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_DENOM_IN)?;
let amount_out_value = get_attr_value_as_u128(bolt_event, BOLT_SWAP_EVENT_AMOUNT_OUT)?;
let denom_out_value = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_DENOM_OUT)?;
let protocol_fee_value =
get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_PROTOCOL_FEE_AMOUNT)?;
let lp_fee_value = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_LP_FEE_AMOUNT)?;
let price = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_PRICE)?;
let market_address_value = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_CONTRACT_ADDRESS)?;
let swap_sender_value = get_attr_value_as_str(bolt_event, BOLT_SWAP_EVENT_SENDER)?;
let trade_response = TradeResponse {
base_out: cosmwasm_std::Coin::new(amount_out_value, denom_out_value),
quote_in: cosmwasm_std::Coin::new(amount_in_value, denom_in_value),
market_address: market_address_value.to_string(),
protocol_fee: Decimal256::from_str(protocol_fee_value)?,
lp_fee: Decimal256::from_str(lp_fee_value)?,
price: Decimal256::from_str(price)?,
sender: swap_sender_value.to_string(),
trade_block_height: block_height,
timestamp: block_time.map(|block_time| block_time.into()),
};
Ok(trade_response)
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TradeResponse {
pub base_out: cosmwasm_std::Coin,
pub quote_in: cosmwasm_std::Coin,
pub market_address: String,
pub protocol_fee: Decimal256,
pub lp_fee: Decimal256,
pub price: Decimal256,
pub sender: String,
pub trade_block_height: u64,
pub timestamp: Option<Timestamp>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::market::client::MarketAdminClient;
use crate::oracle::client::OracleAdminClient;
use crate::router::client::RouterAdminClient;
use crate::test_utils::helpers::{
TEST_ASSET_ARCH_SYMBOL, TEST_ASSET_ETH_SYMBOL, TEST_ASSET_USDT_SYMBOL, VALID_BECH32_ADDRESS,
};
use crate::test_utils::test_scenario::TestScenario;
use cosmrs::{AccountId, Coin};
use cosmwasm_std::{Decimal256, Uint128};
use serial_test::serial;
use std::ops::Add;
use std::str::FromStr;
use std::thread::sleep;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[tokio::test]
#[serial]
async fn non_existent_tx_hash() {
let test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
let router_client = RouterClient::new(
test_scenario.cosmos_client,
VALID_BECH32_ADDRESS.parse().unwrap(), )
.expect("Failed to create router client");
let tx_hash =
"F82D85DB51EB2B0ED25123FE81816B73DC8567F98FDDCF56413F2B6811D6FBAB".to_string();
let err = router_client
.get_trade(tx_hash)
.await
.expect_err("Expected error as the tx is not a swap transaction");
assert!(matches!(err, RouterError::CosmosClientError(_)));
}
#[tokio::test]
#[serial]
async fn non_swap_tx_hash() {
let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
let price_threshold_ratio = Decimal256::from_str("0.5").unwrap();
let price_expire_millis = Some(1000);
let oracle_contract_address = test_scenario
.instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
.await;
let default_protocol_fee = Decimal256::from_str("0.1").unwrap();
let default_lp_fee = Decimal256::from_str("0.1").unwrap();
let router_contract_address = test_scenario
.instantiate_router_contract(
oracle_contract_address.clone(),
oracle_contract_address.clone(),
default_protocol_fee,
default_lp_fee,
)
.await;
let oracle_client =
OracleAdminClient::from_scenario(&test_scenario, &oracle_contract_address)
.expect("Failed to create oracle admin client");
let response = test_scenario.set_default_assets(&oracle_client).await;
let router_client = RouterClient::new(
test_scenario.cosmos_client,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router client");
sleep(Duration::from_secs(1));
let tx_hash = response.hash.to_string();
let err = router_client
.get_trade(tx_hash)
.await
.expect_err("Expected error as the tx is not a swap transaction");
assert!(matches!(err, RouterError::NotBoltTransactionError));
}
#[tokio::test]
#[serial]
async fn test_get_trade() {
let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
let price_threshold_ratio = Decimal256::from_str("0.5").unwrap();
let price_expire_millis = Some(1000);
let oracle_contract_address = test_scenario
.instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
.await;
let default_protocol_fee = Decimal256::from_str("0.1").unwrap();
let default_lp_fee = Decimal256::from_str("0.1").unwrap();
let router_contract_address = test_scenario
.instantiate_router_contract(
oracle_contract_address.clone(),
oracle_contract_address.clone(),
default_protocol_fee,
default_lp_fee,
)
.await;
let oracle_client =
OracleAdminClient::from_scenario(&test_scenario, &oracle_contract_address)
.expect("Failed to create oracle admin client");
let price_expiry_time = SystemTime::now().add(Duration::from_secs(7200)); let price_expiry_timestamp = price_expiry_time
.duration_since(UNIX_EPOCH)
.expect("Converting to timestamp failed");
test_scenario.set_default_assets(&oracle_client).await;
test_scenario
.set_default_prices(&oracle_client, price_expiry_timestamp, "50000")
.await;
let router_client = RouterAdminClient::from_scenario(
&test_scenario,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router admin client");
test_scenario
.create_market(
&router_client,
TEST_ASSET_ARCH_SYMBOL,
&[TEST_ASSET_USDT_SYMBOL],
Uint128::new(10),
)
.await;
let market_address = test_scenario
.create_market(
&router_client,
TEST_ASSET_USDT_SYMBOL,
&[TEST_ASSET_ARCH_SYMBOL, TEST_ASSET_ETH_SYMBOL],
Uint128::new(10),
)
.await;
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_address.as_ref()).unwrap(),
&admin_account,
Coin::new(10000000, TEST_ASSET_USDT_SYMBOL).unwrap(),
)
.await;
let want_out = TEST_ASSET_USDT_SYMBOL;
let response = test_scenario
.swap_exact_in(
&router_client,
want_out,
None,
None,
10,
TEST_ASSET_ARCH_SYMBOL.to_string(),
)
.await;
let router_client = RouterClient::new(
test_scenario.cosmos_client,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router client");
sleep(Duration::from_secs(1));
let tx_hash = response.hash.to_string();
let trade_response = router_client
.get_trade(tx_hash)
.await
.expect("Failed to get trade");
assert_eq!(trade_response.base_out.amount.u128(), 405000u128);
assert_eq!(trade_response.base_out.denom, TEST_ASSET_USDT_SYMBOL);
assert_eq!(trade_response.quote_in.amount.u128(), 10u128);
assert_eq!(trade_response.quote_in.denom, TEST_ASSET_ARCH_SYMBOL);
assert_eq!(trade_response.market_address, market_address);
assert_eq!(
trade_response.protocol_fee,
Decimal256::from_str("1").unwrap()
);
assert_eq!(
trade_response.lp_fee,
Decimal256::from_str("45000").unwrap()
);
assert_eq!(
trade_response.price,
Decimal256::from_str("0.00002").unwrap()
); }
#[tokio::test]
#[serial]
async fn get_trades_in_block_no_trades() {
let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
let price_threshold_ratio = Decimal256::from_str("0.5").unwrap();
let price_expire_millis = Some(1000);
let oracle_contract_address = test_scenario
.instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
.await;
let default_protocol_fee = Decimal256::from_str("0.1").unwrap();
let default_lp_fee = Decimal256::from_str("0.1").unwrap();
let router_contract_address = test_scenario
.instantiate_router_contract(
oracle_contract_address.clone(),
oracle_contract_address.clone(),
default_protocol_fee,
default_lp_fee,
)
.await;
let router_client = RouterAdminClient::from_scenario(
&test_scenario,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router admin client");
let trades_response = router_client
.public_router_client
.get_trades_in_block(100)
.await
.expect("Failed to get trade");
assert_eq!(trades_response.len(), 0);
}
#[tokio::test]
#[serial]
async fn get_trades_in_block() {
let mut test_scenario = TestScenario::new_from_config("config.json".to_string()).await;
let price_threshold_ratio = Decimal256::from_str("0.5").unwrap();
let price_expire_millis = Some(1000);
let oracle_contract_address = test_scenario
.instantiate_oracle_contract(price_threshold_ratio, price_expire_millis)
.await;
let default_protocol_fee = Decimal256::from_str("0.1").unwrap();
let default_lp_fee = Decimal256::from_str("0.1").unwrap();
let router_contract_address = test_scenario
.instantiate_router_contract(
oracle_contract_address.clone(),
oracle_contract_address.clone(),
default_protocol_fee,
default_lp_fee,
)
.await;
let oracle_client =
OracleAdminClient::from_scenario(&test_scenario, &oracle_contract_address)
.expect("Failed to create oracle admin client");
let price_expiry_time = SystemTime::now().add(Duration::from_secs(7200)); let price_expiry_timestamp = price_expiry_time
.duration_since(UNIX_EPOCH)
.expect("Converting to timestamp failed");
test_scenario.set_default_assets(&oracle_client).await;
test_scenario
.set_default_prices(&oracle_client, price_expiry_timestamp, "50000")
.await;
let router_client = RouterAdminClient::from_scenario(
&test_scenario,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router admin client");
test_scenario
.create_market(
&router_client,
TEST_ASSET_ARCH_SYMBOL,
&[TEST_ASSET_USDT_SYMBOL],
Uint128::new(10),
)
.await;
let market_address = test_scenario
.create_market(
&router_client,
TEST_ASSET_USDT_SYMBOL,
&[TEST_ASSET_ARCH_SYMBOL, TEST_ASSET_ETH_SYMBOL],
Uint128::new(10),
)
.await;
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_address.as_ref()).unwrap(),
&admin_account,
Coin::new(10000000, TEST_ASSET_USDT_SYMBOL).unwrap(),
)
.await;
let want_out = TEST_ASSET_USDT_SYMBOL;
let response = test_scenario
.swap_exact_in(
&router_client,
want_out,
None,
None,
10,
TEST_ASSET_ARCH_SYMBOL.to_string(),
)
.await;
let router_client = RouterClient::new(
test_scenario.cosmos_client,
router_contract_address.parse().unwrap(),
)
.expect("Failed to create router client");
sleep(Duration::from_secs(1));
let block_height = response.height.value();
let trades_response = router_client
.get_trades_in_block(block_height)
.await
.expect("Failed to get trade");
assert_eq!(trades_response.len(), 1);
let trade_response = trades_response.first().unwrap();
assert_eq!(trade_response.base_out.amount.u128(), 405000u128);
assert_eq!(trade_response.base_out.denom, TEST_ASSET_USDT_SYMBOL);
assert_eq!(trade_response.quote_in.amount.u128(), 10u128);
assert_eq!(trade_response.quote_in.denom, TEST_ASSET_ARCH_SYMBOL);
assert_eq!(trade_response.market_address, market_address);
assert_eq!(
trade_response.protocol_fee,
Decimal256::from_str("1").unwrap()
);
assert_eq!(
trade_response.lp_fee,
Decimal256::from_str("45000").unwrap()
);
assert_eq!(
trade_response.price,
Decimal256::from_str("0.00002").unwrap()
); }
}