use crate::asset::{Asset, AssetInfo, DecimalAsset};
use crate::error::ContractError;
use crate::vault::FEE_PRECISION;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal, Decimal256, DepsMut, Env, Event,
MessageInfo, Response, StdError, StdResult, Uint128, WasmMsg,
};
use cw20_base::msg::ExecuteMsg as CW20ExecuteMsg;
use cw_storage_plus::Item;
use itertools::Itertools;
pub const MAX_LIMIT: u32 = 30;
pub const DEFAULT_LIMIT: u32 = 10;
const ATTR_SENDER: &str = "sender";
pub trait EventExt {
fn from_info(name: impl Into<String>, info: &MessageInfo) -> Event;
fn from_sender(name: impl Into<String>, sender: impl Into<String>) -> Event;
}
impl EventExt for Event {
fn from_info(name: impl Into<String>, info: &MessageInfo) -> Event {
Event::new(name).add_attribute(ATTR_SENDER, info.sender.to_string())
}
fn from_sender(name: impl Into<String>, sender: impl Into<String>) -> Event {
Event::new(name).add_attribute(ATTR_SENDER, sender)
}
}
#[cw_serde]
pub struct OwnershipProposal {
pub owner: Addr,
pub ttl: u64,
}
pub fn propose_new_owner(
deps: DepsMut,
info: MessageInfo,
env: Env,
new_owner: String,
expires_in: u64,
owner: Addr,
proposal: Item<OwnershipProposal>,
contract_name: impl Into<String>,
) -> StdResult<Response> {
if info.sender != owner {
return Err(StdError::generic_err("Unauthorized"));
}
let new_owner = deps.api.addr_validate(new_owner.as_str())?;
if new_owner == owner {
return Err(StdError::generic_err("New owner cannot be same"));
}
proposal.save(
deps.storage,
&OwnershipProposal {
owner: new_owner.clone(),
ttl: env.block.time.seconds() + expires_in,
},
)?;
Ok(Response::new().add_event(
Event::from_info(contract_name.into() + "::propose_new_owner" , &info)
.add_attribute("new_owner", new_owner)
.add_attribute("expires_in", expires_in.to_string())
))
}
pub fn drop_ownership_proposal(
deps: DepsMut,
info: MessageInfo,
owner: Addr,
proposal: Item<OwnershipProposal>,
contract_name: impl Into<String>,
) -> StdResult<Response> {
if info.sender != owner {
return Err(StdError::generic_err("Unauthorized"));
}
proposal.remove(deps.storage);
Ok(Response::new().add_event(Event::from_info(contract_name.into() + "::drop_ownership_proposal", &info)))
}
pub fn claim_ownership(
deps: DepsMut,
info: MessageInfo,
env: Env,
ownership_proposal: Item<OwnershipProposal>,
callback: fn(DepsMut, Addr) -> StdResult<()>,
contract_name: impl Into<String>,
) -> StdResult<Response> {
let proposal: OwnershipProposal = ownership_proposal
.load(deps.storage)
.map_err(|_| StdError::generic_err("Ownership proposal not found"))?;
if info.sender != proposal.owner {
return Err(StdError::generic_err("Unauthorized"));
}
if env.block.time.seconds() > proposal.ttl {
return Err(StdError::generic_err("Ownership proposal expired"));
}
ownership_proposal.remove(deps.storage);
callback(deps, proposal.owner.clone())?;
Ok(Response::new().add_event(Event::from_info(contract_name.into() + "::claim_ownership", &info)))
}
pub fn build_transfer_cw20_token_msg(
recipient: Addr,
token_contract_address: String,
amount: Uint128,
) -> StdResult<CosmosMsg> {
Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: token_contract_address,
msg: to_json_binary(&CW20ExecuteMsg::Transfer {
recipient: recipient.into(),
amount,
})?,
funds: vec![],
}))
}
pub fn build_send_native_asset_msg(
recipient: Addr,
denom: &str,
amount: Uint128,
) -> StdResult<CosmosMsg> {
Ok(CosmosMsg::Bank(BankMsg::Send {
to_address: recipient.into(),
amount: vec![Coin {
denom: denom.to_string(),
amount,
}],
}))
}
pub fn build_transfer_cw20_from_user_msg(
cw20_token_address: String,
owner: String,
recipient: String,
amount: Uint128,
) -> StdResult<CosmosMsg> {
Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: cw20_token_address,
funds: vec![],
msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom {
owner,
recipient,
amount,
})?,
}))
}
pub fn build_transfer_token_to_user_msg(
asset: AssetInfo,
recipient: Addr,
amount: Uint128,
) -> StdResult<CosmosMsg> {
match asset {
AssetInfo::NativeToken { denom } => {
Ok(build_send_native_asset_msg(recipient, &denom, amount)?)
}
AssetInfo::Token { contract_addr } => Ok(build_transfer_cw20_token_msg(
recipient,
contract_addr.to_string(),
amount,
)?),
}
}
pub fn select_pools(
offer_asset_info: &AssetInfo,
ask_asset_info: &AssetInfo,
pools: &[DecimalAsset],
) -> Result<(DecimalAsset, DecimalAsset), ContractError> {
if pools.len() == 2 {
let (offer_ind, offer_pool) = pools
.iter()
.find_position(|pool| pool.info.eq(offer_asset_info))
.ok_or(ContractError::AssetMismatch {})?;
let ask_pool = pools[(offer_ind + 1) % 2].clone();
if !ask_pool.info.eq(ask_asset_info) {
return Err(ContractError::AssetMismatch {});
}
Ok((offer_pool.clone(), ask_pool))
} else {
if ask_asset_info.eq(offer_asset_info) {
return Err(ContractError::SameAssets {});
}
let offer_pool = pools
.iter()
.find(|pool| pool.info.eq(offer_asset_info))
.ok_or(ContractError::AssetMismatch {})?;
let ask_pool = pools
.iter()
.find(|pool| pool.info.eq(ask_asset_info))
.ok_or(ContractError::AssetMismatch {})?;
Ok((offer_pool.clone(), ask_pool.clone()))
}
}
pub fn check_swap_parameters(
offer_amount: Uint128,
ask_amount: Uint128,
swap_amount: Uint128,
) -> StdResult<()> {
if offer_amount.is_zero() || ask_amount.is_zero() {
return Err(StdError::generic_err("One of the pools is empty"));
}
if swap_amount.is_zero() {
return Err(StdError::generic_err("Swap amount must not be zero"));
}
Ok(())
}
pub fn get_share_in_assets(pools: Vec<Asset>, amount: Uint128, total_share: Uint128) -> Vec<Asset> {
let mut share_ratio = Decimal::zero();
if !total_share.is_zero() {
share_ratio = Decimal::from_ratio(amount, total_share);
}
pools
.iter()
.map(|a| Asset {
info: a.info.clone(),
amount: a.amount * share_ratio,
})
.collect()
}
pub fn decimal_to_decimal256(dec_value: Decimal) -> StdResult<Decimal256> {
Decimal256::from_atomics(dec_value.atomics(), dec_value.decimal_places()).map_err(|_| {
StdError::generic_err(format!(
"Failed to convert Decimal {} to Decimal256",
dec_value
))
})
}
pub fn decimal256_to_decimal(dec_value: Decimal256) -> StdResult<Decimal> {
let value: Uint128 = dec_value.atomics().try_into()
.map_err(|_| StdError::generic_err(format!("Failed to convert Decimal256 {} to Decimal", dec_value)))?;
Decimal::from_atomics(value, dec_value.decimal_places()).map_err(|_| {
StdError::generic_err(format!(
"Failed to convert Decimal256 {} to Decimal",
dec_value
))
})
}
pub fn adjust_precision(
value: Uint128,
current_precision: u8,
new_precision: u8,
) -> StdResult<Uint128> {
Ok(match current_precision.cmp(&new_precision) {
std::cmp::Ordering::Equal => value,
std::cmp::Ordering::Less => value.checked_mul(Uint128::new(
10_u128.pow((new_precision - current_precision) as u32),
))?,
std::cmp::Ordering::Greater => value.checked_div(Uint128::new(
10_u128.pow((current_precision - new_precision) as u32),
))?,
})
}
pub fn get_lp_token_name(pool_id: Uint128) -> String {
let token_name = pool_id.to_string() + "-Dex-LP".to_string().as_str();
return token_name;
}
pub fn get_lp_token_symbol() -> String {
return "DEX-LP".to_string();
}
pub fn is_valid_name(name: &str) -> bool {
let bytes = name.as_bytes();
if bytes.len() < 3 || bytes.len() > 50 {
return false;
}
true
}
pub fn is_valid_symbol(symbol: &str) -> bool {
let bytes = symbol.as_bytes();
if bytes.len() < 3 || bytes.len() > 12 {
return false;
}
for byte in bytes.iter() {
if (*byte != 45) && (*byte < 65 || *byte > 90) && (*byte < 97 || *byte > 122) {
return false;
}
}
true
}
pub fn find_sent_native_token_balance(message_info: &MessageInfo, denom: &str) -> Uint128 {
message_info
.funds
.iter()
.find(|x| x.denom == denom)
.map(|x| x.amount)
.unwrap_or(Uint128::zero())
}
pub fn calculate_underlying_fees(amount: Uint128, total_fee_bps: u16) -> Uint128 {
amount * Decimal::from_ratio(total_fee_bps, FEE_PRECISION)
}