pub mod msgs {
use std::marker::PhantomData;
use cosmwasm_schema::{cw_serde, QueryResponses};
use cosmwasm_std::{Binary, Decimal, Uint128};
use cw_asset::Asset;
use schemars::JsonSchema;
use super::{
definitions::{Config, TickModel, UserPosition},
swap_module::SwapInterfaceCollector,
};
#[cw_serde]
pub struct InstantiateMsg {
pub owner: String,
pub address_provider: String,
pub fee_swap: Decimal,
}
#[cw_serde]
pub struct MigrateMsg {}
#[cw_serde]
pub enum ExecuteMsg<SIC: SwapInterfaceCollector> {
SetGrid(SetGridMsg<SIC>),
UpdateGrid(UpdateGridMsg<SIC>),
RemoveGrid(RemoveGridMsg),
Trigger(TriggerMsg),
ExecuteActionFeature(ActionFeatureMsg),
UpdateConfig(UpdateConfigMsg),
}
#[cw_serde]
pub struct SetGridMsg<SICollector: SwapInterfaceCollector> {
pub base_asset: Asset,
pub quote_asset: Asset,
pub lower_bound: Decimal,
pub upper_bound: Decimal,
pub tick_model: TickModel,
pub swap_module: SICollector,
}
#[cw_serde]
pub struct UpdateGridMsg<SICollector: SwapInterfaceCollector> {
pub id: u64,
pub modify_base_amount: Option<ModifyAmount>,
pub modify_quote_amount: Option<ModifyAmount>,
pub lower_bound: Option<Decimal>,
pub upper_bound: Option<Decimal>,
pub tick_model: Option<TickModel>,
pub swap_module: Option<SICollector>,
}
#[cw_serde]
pub struct RemoveGridMsg {
pub id: u64,
}
#[cw_serde]
pub struct TriggerMsg {
pub id: u64,
}
#[cw_serde]
pub struct ActionFeatureMsg {
pub feature: Binary,
}
#[cw_serde]
pub struct UpdateConfigMsg {
pub owner: Option<String>,
pub variable_provider: Option<String>,
pub fee_swap: Option<Decimal>,
}
#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg<SIC: SwapInterfaceCollector, AIUI: JsonSchema> {
#[returns(Config)]
Config {},
#[returns(Config)]
ActionAvailable { id: u64 },
#[returns(UserPosition<SIC>)]
UserPosition { id: u64 },
#[returns(Vec<UserPosition<SIC>>)]
UserPositions {
user: String,
start_after: Option<u64>,
limit: Option<u32>,
},
#[returns(AIUI)]
UserActionInfo { user: String },
#[returns(String)]
Phantom {
sic: PhantomData<SIC>,
ai: PhantomData<AIUI>,
},
}
#[cw_serde]
pub enum ModifyAmount {
Deposit(Uint128),
Withdraw(Uint128),
}
#[cw_serde]
pub struct ActionAvailableResponse {
pub action_available: bool,
}
}
pub mod definitions {
use std::cmp::{max, min};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
Addr, CosmosMsg, Decimal, Decimal256, Deps, StdError, StdResult, Uint128, Uint256,
};
use cw_asset::Asset;
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
use enum_repr::EnumRepr;
use rhaki_cw_plus::{
math::{IntoDecimal, IntoUint},
traits::IntoStdResult,
};
use serde::{de::DeserializeOwned, Serialize};
use crate::variable_provider::{
definitions::variable_provider_get_variable, keys::BS_FEE_COLLECTOR,
};
use super::{msgs::SetGridMsg, swap_module::SwapInterfaceCollector};
#[cw_serde]
pub struct Config {
pub owner: Addr,
pub variable_provider: Addr,
pub counter_positions: u64,
pub fee_swap: Decimal,
}
impl Config {
pub fn new(owner: Addr, address_provider: Addr, fee_swap: Decimal) -> Config {
Config {
owner,
variable_provider: address_provider,
counter_positions: 0,
fee_swap,
}
}
pub fn validate(&self, deps: Deps) -> StdResult<()> {
if self.fee_swap > Decimal::one() {
return Err(StdError::generic_err(format!(
"Invlid fee_swap (gte 1): {}",
self.fee_swap
)));
}
if self.fee_swap > Decimal::zero() {
variable_provider_get_variable(deps, BS_FEE_COLLECTOR, &self.variable_provider)?
.unwrap_addr()?;
}
Ok(())
}
}
#[cw_serde]
pub struct UserPosition<SIC: SwapInterfaceCollector> {
pub id: u64,
pub owner: Addr,
pub quote_asset: Asset,
pub base_asset: Asset,
pub lower_bound: Decimal,
pub upper_bound: Decimal,
pub current_tick: Decimal,
pub tick_model: TickModel,
pub swap_module: SIC,
}
impl<SIC> UserPosition<SIC>
where
SIC: SwapInterfaceCollector,
{
pub fn new<SICollector: SwapInterfaceCollector>(
deps: Deps,
config: &Config,
id: u64,
owner: Addr,
msg: SetGridMsg<SICollector>,
) -> StdResult<UserPosition<SICollector>> {
let mut user_position = UserPosition {
id,
owner,
quote_asset: msg.quote_asset,
base_asset: msg.base_asset,
lower_bound: msg.lower_bound,
upper_bound: msg.upper_bound,
current_tick: Decimal::zero(),
tick_model: msg.tick_model,
swap_module: msg.swap_module,
};
user_position.validate(deps, config)?;
Ok(user_position)
}
pub fn validate(&mut self, deps: Deps, config: &Config) -> StdResult<()> {
self.upper_bound = self
.tick_model
.fix_upper_bound(self.lower_bound, self.upper_bound);
self.set_current_tick()?;
self.swap_module.inner().validate_grid_creation(
deps,
config,
[self.base_asset.clone(), self.quote_asset.clone()],
)?;
Ok(())
}
pub fn check_next_action(&self, deps: Deps, config: &Config) -> StdResult<bool> {
self.try_action(deps, config)
.map(|val| val.map(|_| true).unwrap_or(false))
}
pub fn do_next_action(
&self,
deps: Deps,
config: &Config,
) -> StdResult<Option<(CosmosMsg, SwapDirection)>> {
self.try_action(deps, config)?
.map(|val| (self.do_action(deps, config, val)))
.transpose()
}
pub fn set_current_tick(&mut self) -> StdResult<()> {
self.current_tick = self.tick_model.calculate_current_tick(
self.lower_bound,
self.upper_bound,
self.base_asset.amount,
self.quote_asset.amount,
)?;
Ok(())
}
fn try_action(&self, deps: Deps, config: &Config) -> StdResult<Option<SwapDirection>> {
if self.base_asset.amount > Uint128::zero() {
let (base_to_sell, minimum_receive) = self.calculate_sell_amount()?;
let return_amount = self.swap_module.inner().simulate_swap(
deps,
config,
Asset::new(self.base_asset.info.clone(), base_to_sell),
self.quote_asset.info.clone(),
minimum_receive,
)?;
if return_amount >= minimum_receive {
return Ok(Some(SwapDirection::Sell));
}
}
if self.quote_asset.amount > Uint128::zero() {
let (quote_to_sell, minimum_receive) = self.calculate_buy_amount()?;
let return_amount = self.swap_module.inner().simulate_swap(
deps,
config,
Asset::new(self.quote_asset.info.clone(), quote_to_sell),
self.base_asset.info.clone(),
minimum_receive,
)?;
if return_amount >= minimum_receive {
return Ok(Some(SwapDirection::Buy));
}
}
Ok(None)
}
fn do_action(
&self,
deps: Deps,
config: &Config,
action: SwapDirection,
) -> StdResult<(CosmosMsg, SwapDirection)> {
match action {
SwapDirection::Sell {} => {
let (base_to_sell, minimum_receive) = self.calculate_sell_amount()?;
self.swap_module.inner().build_swap_msg(
deps,
config,
Asset::new(self.base_asset.info.clone(), base_to_sell),
self.quote_asset.info.clone(),
minimum_receive,
)
}
SwapDirection::Buy {} => {
let (quote_to_sell, minimum_receive) = self.calculate_buy_amount()?;
self.swap_module.inner().build_swap_msg(
deps,
config,
Asset::new(self.quote_asset.info.clone(), quote_to_sell),
self.base_asset.info.clone(),
minimum_receive,
)
}
}
.map(|swap_msg| (swap_msg, action))
}
fn calculate_sell_amount(&self) -> StdResult<(Uint128, Uint128)> {
self.tick_model.calculate_sell_amount(
self.current_tick,
self.lower_bound,
self.upper_bound,
self.base_asset.amount,
self.quote_asset.amount,
)
}
fn calculate_buy_amount(&self) -> StdResult<(Uint128, Uint128)> {
self.tick_model.calculate_buy_amount(
self.current_tick,
self.lower_bound,
self.upper_bound,
self.base_asset.amount,
self.quote_asset.amount,
)
}
}
#[cw_serde]
pub enum TickModel {
Linear { tick_distance: Decimal },
}
impl TickModel {
pub fn fix_upper_bound(&self, lower_bound: Decimal, upper_bound: Decimal) -> Decimal {
match self {
TickModel::Linear { tick_distance } => {
TickModel::linear_fix_upper_bound(*tick_distance, lower_bound, upper_bound)
}
}
}
pub fn calculate_current_tick(
&self,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<Decimal> {
match self {
TickModel::Linear { tick_distance } => {
let raw =
self.linear_calculate_current_tick(lower_bound, upper_bound, base, quote)?;
Ok(TickModel::linear_fix_upper_bound(
*tick_distance,
lower_bound,
raw,
))
}
}
}
pub fn calculate_sell_amount(
&self,
current_tick: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<(Uint128, Uint128)> {
match self {
TickModel::Linear { tick_distance } => TickModel::linear_calculate_sell_amount(
*tick_distance,
current_tick,
lower_bound,
upper_bound,
base,
quote,
),
}
}
pub fn calculate_buy_amount(
&self,
current_tick: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<(Uint128, Uint128)> {
match self {
TickModel::Linear { tick_distance } => TickModel::linear_calculate_buy_amount(
*tick_distance,
current_tick,
lower_bound,
upper_bound,
base,
quote,
),
}
}
fn linear_fix_upper_bound(
tick_distance: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
) -> Decimal {
((upper_bound - lower_bound) / tick_distance + Decimal::from_ratio(1u128, 2u128))
.floor()
* tick_distance
+ lower_bound
}
fn linear_calculate_current_tick(
&self,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<Decimal> {
if base.is_zero() {
return Ok(upper_bound);
} else if quote.is_zero() {
return Ok(lower_bound);
}
let b: Uint256 = base.into();
let q: Uint256 = quote.into();
let l: Decimal256 = lower_bound.into();
let u: Decimal256 = upper_bound.into();
let left_under_sqrt = if b * l > q {
(b * l - q).pow(2)
} else {
(q - b * l).pow(2)
};
let right_under_sqrt = 4_u128.into_uint256() * b * q * u;
let under_sqrt = (left_under_sqrt + right_under_sqrt)
.into_decimal_256()
.sqrt();
let numerator = b * l + under_sqrt.into_uint256() - q;
let denominator = 2_u128.into_uint256() * b;
Decimal256::from_ratio(numerator, denominator)
.try_into()
.into_std_result()
}
fn linear_calculate_sell_amount(
tick_distance: Decimal,
current_tick: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<(Uint128, Uint128)> {
let price = min(current_tick + tick_distance, upper_bound);
let base_share = TickModel::linear_share_base_at_price(price, lower_bound, upper_bound);
let (next_base, _) =
TickModel::linear_compute_amounts_at_target_ratio(base_share, price, base, quote);
let base_to_sell = base - next_base;
if base_to_sell == Uint128::zero() {
return Err(StdError::generic_err("base to sell = 0"));
}
let minimum_receive = base_to_sell * price;
Ok((base_to_sell, minimum_receive))
}
fn linear_calculate_buy_amount(
tick_distance: Decimal,
current_tick: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
base: Uint128,
quote: Uint128,
) -> StdResult<(Uint128, Uint128)> {
let price = max(current_tick - tick_distance, lower_bound);
let base_share = TickModel::linear_share_base_at_price(price, lower_bound, upper_bound);
let (_, next_quote) =
TickModel::linear_compute_amounts_at_target_ratio(base_share, price, base, quote);
let quote_to_sell = quote - next_quote;
if quote_to_sell == Uint128::zero() {
return Err(StdError::generic_err("quote to sell = 0"));
}
let minimum_receive = quote_to_sell * (Decimal::one() / price);
Ok((quote_to_sell, minimum_receive))
}
fn linear_share_base_at_price(
price: Decimal,
lower_bound: Decimal,
upper_bound: Decimal,
) -> Decimal {
Decimal::one()
- min(
(price - lower_bound) / (upper_bound - lower_bound),
Decimal::one(),
)
}
fn linear_compute_amounts_at_target_ratio(
base_share: Decimal,
price: Decimal,
base: Uint128,
quote: Uint128,
) -> (Uint128, Uint128) {
let total_value = base * price + quote;
let next_base_in_quote = total_value * base_share;
let next_quote = total_value - next_base_in_quote;
let next_base = next_base_in_quote * (Decimal::one() / price);
(next_base, next_quote)
}
}
#[cw_serde]
pub enum SwapDirection {
Sell,
Buy,
}
#[cw_serde]
pub struct UserPositionInfo<SIC: SwapInterfaceCollector, T> {
pub base_info: UserPosition<SIC>,
pub action_info: T,
}
pub struct UserPositionIndexes<'a, SIC: SwapInterfaceCollector> {
pub owner: MultiIndex<'a, Addr, UserPosition<SIC>, u64>,
}
impl<'a, SIC> IndexList<UserPosition<SIC>> for UserPositionIndexes<'a, SIC>
where
SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned,
{
fn get_indexes(
&'_ self,
) -> Box<dyn Iterator<Item = &'_ dyn Index<UserPosition<SIC>>> + '_> {
let v: Vec<&dyn Index<UserPosition<SIC>>> = vec![&self.owner];
Box::new(v.into_iter())
}
}
#[allow(non_snake_case)]
pub fn USER_POSITIONS<
'a,
SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned,
>() -> IndexedMap<'a, u64, UserPosition<SIC>, UserPositionIndexes<'a, SIC>> {
let indexes = UserPositionIndexes {
owner: MultiIndex::new(
|_, val| val.owner.clone(),
"ns_user_positions",
"ns_user_positions_owner",
),
};
IndexedMap::new("ns_user_positions", indexes)
}
#[EnumRepr(type = "u64")]
pub enum ModulesReplyID {
InitMiddle = 101,
InitWarp = 102,
Ignore = 104,
}
}
pub mod swap_module {
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Coin, CosmosMsg, Deps, StdResult, Uint128, WasmMsg};
use cw_asset::{Asset, AssetInfo};
use rhaki_cw_plus::wasm::{CosmosMsgBuilder, WasmMsgBuilder};
use super::definitions::Config;
pub trait SwapInterface {
fn simulate_swap(
&self,
deps: Deps,
config: &Config,
input_asset: Asset,
output_asset: AssetInfo,
minimum_receive: Uint128,
) -> StdResult<Uint128>;
fn build_swap_msg(
&self,
deps: Deps,
config: &Config,
input_asset: Asset,
output_asset: AssetInfo,
minimum_receive: Uint128,
) -> StdResult<CosmosMsg>;
fn validate_grid_creation(
&self,
deps: Deps,
config: &Config,
received_asset: [Asset; 2],
) -> StdResult<()>;
}
pub trait SwapInterfaceCollector {
fn inner(&self) -> &dyn SwapInterface;
}
pub mod si_astroport {
use super::*;
use astroport::{asset::AssetInfo as AstroportAssetInfo, router::SwapOperation};
use cosmwasm_std::{to_json_binary, Addr, StdError};
use rhaki_cw_plus::traits::IntoInner;
use crate::variable_provider::{
definitions::variable_provider_get_variable, keys::ASTROPORT_ROUTER,
};
pub const AMOUNT_SIMULATE_ON_ZERO_AMOUNT: u128 = 1_000_000_u128;
pub trait Reversable {
fn reverse_all(&self) -> Self;
}
impl Reversable for Vec<SwapOperation> {
fn reverse_all(&self) -> Self {
let mut clone = self.clone();
clone.reverse();
clone
.into_iter()
.map(|asset_info| match asset_info {
SwapOperation::NativeSwap {
offer_denom,
ask_denom,
} => SwapOperation::NativeSwap {
offer_denom: ask_denom,
ask_denom: offer_denom,
},
SwapOperation::AstroSwap {
offer_asset_info,
ask_asset_info,
} => SwapOperation::AstroSwap {
offer_asset_info: ask_asset_info,
ask_asset_info: offer_asset_info,
},
})
.collect()
}
}
#[cw_serde]
pub struct SIAstroport {}
impl SIAstroport {
fn get_astroport_router(&self, deps: Deps, config: &Config) -> StdResult<Addr> {
variable_provider_get_variable(
deps,
ASTROPORT_ROUTER.to_string(),
&config.variable_provider,
)?
.unwrap_addr()
}
fn build_swap_operation(
&self,
deps: Deps,
config: &Config,
offer: String,
ask: String,
) -> StdResult<Vec<SwapOperation>> {
let key = vp_get_astroport_router_key(&offer, &ask);
let route = variable_provider_get_variable(deps, key, &config.variable_provider)?
.unwrap_binary::<Vec<SwapOperation>>()?;
let (_, route) = vp_validate_key_route(&offer, &ask, route)?;
Ok(route)
}
}
impl SwapInterface for SIAstroport {
fn simulate_swap(
&self,
deps: Deps,
config: &Config,
input_asset: Asset,
output_asset: AssetInfo,
_minimum_receive: Uint128,
) -> StdResult<Uint128> {
let router = self.get_astroport_router(deps, config)?;
let swap_operations = self.build_swap_operation(
deps,
config,
input_asset.info.inner(),
output_asset.inner(),
)?;
deps.querier
.query_wasm_smart::<astroport::router::SimulateSwapOperationsResponse>(
router,
&astroport::router::QueryMsg::SimulateSwapOperations {
offer_amount: input_asset.amount,
operations: swap_operations,
},
)
.map(|val| val.amount)
}
fn build_swap_msg(
&self,
deps: Deps,
config: &Config,
input_asset: Asset,
output_asset: AssetInfo,
minimum_receive: Uint128,
) -> StdResult<CosmosMsg> {
let router = variable_provider_get_variable(
deps,
ASTROPORT_ROUTER.to_string(),
&config.variable_provider,
)?
.unwrap_addr()?;
let swap_operations = self.build_swap_operation(
deps,
config,
input_asset.info.inner(),
output_asset.inner(),
)?;
match input_asset.info.clone() {
cw_asset::AssetInfoBase::Native(denom) => WasmMsg::build_execute(
router,
astroport::router::ExecuteMsg::ExecuteSwapOperations {
operations: swap_operations,
minimum_receive: Some(minimum_receive),
to: None,
max_spread: None,
},
vec![Coin::new(input_asset.amount.u128(), denom)],
),
cw_asset::AssetInfoBase::Cw20(cw20_addr) => WasmMsg::build_execute(
cw20_addr,
cw20::Cw20ExecuteMsg::Send {
contract: router.to_string(),
amount: input_asset.amount,
msg: to_json_binary(
&astroport::router::ExecuteMsg::ExecuteSwapOperations {
operations: swap_operations,
minimum_receive: Some(minimum_receive),
to: None,
max_spread: None,
},
)?,
},
vec![],
),
_ => todo!(),
}
.map(|val| val.into_cosmos_msg())
}
fn validate_grid_creation(
&self,
deps: Deps,
config: &Config,
received_assets: [Asset; 2],
) -> StdResult<()> {
for (i, asset) in received_assets.iter().enumerate() {
self.simulate_swap(
deps,
config,
Asset::new(
asset.info.clone(),
if asset.amount == Uint128::zero() {
AMOUNT_SIMULATE_ON_ZERO_AMOUNT
} else {
asset.amount.u128()
},
),
received_assets[i ^ 1].info.clone(),
Uint128::zero(),
)?;
}
Ok(())
}
}
pub fn vp_get_astroport_router_key(offer: &str, ask: &str) -> String {
let mut ordered = [&offer, &ask];
ordered.sort();
format!("astro_route_{}_{}", ordered[0], ordered[1])
}
pub fn vp_validate_key_route(
offer: &str,
ask: &str,
route: Vec<SwapOperation>,
) -> StdResult<(String, Vec<SwapOperation>)> {
let key = vp_get_astroport_router_key(offer, ask);
let first_offer_on_route =
get_offer_asset_inner_from_swap_operation(route.first().unwrap());
let last_ask_on_route = get_ask_asset_inner_from_swap_operation(route.last().unwrap());
let mut route = route;
if first_offer_on_route == offer && last_ask_on_route == ask {
} else if first_offer_on_route == ask && last_ask_on_route == offer {
route = route.reverse_all()
} else {
return Err(StdError::generic_err(format!(
"Invalid route with offer: {offer} ask: {ask} route: {route:#?}"
)));
}
Ok((key, route))
}
pub fn get_offer_asset_inner_from_swap_operation(operation: &SwapOperation) -> String {
match operation {
SwapOperation::NativeSwap { offer_denom, .. } => offer_denom.clone(),
SwapOperation::AstroSwap {
offer_asset_info, ..
} => match offer_asset_info {
AstroportAssetInfo::Token { contract_addr } => contract_addr.to_string(),
AstroportAssetInfo::NativeToken { denom } => denom.clone(),
},
}
}
pub fn get_ask_asset_inner_from_swap_operation(operation: &SwapOperation) -> String {
match operation {
SwapOperation::NativeSwap { ask_denom, .. } => ask_denom.clone(),
SwapOperation::AstroSwap { ask_asset_info, .. } => match ask_asset_info {
AstroportAssetInfo::Token { contract_addr } => contract_addr.to_string(),
AstroportAssetInfo::NativeToken { denom } => denom.clone(),
},
}
}
}
}
pub mod action_module {
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, StdError,
StdResult, SubMsg, Uint128, WasmMsg,
};
use cw_asset::AssetInfo;
use cw_storage_plus::Map;
use rhaki_cw_plus::wasm::{CosmosMsgBuilder, WasmMsgBuilder};
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Serialize};
use crate::variable_provider::{
definitions::variable_provider_get_variable, keys::WARP_CONTROLLER,
};
use super::{
definitions::{Config, UserPosition},
swap_module::SwapInterfaceCollector,
};
pub trait ActionInterface {
type UserInfo: Serialize + JsonSchema;
fn on_init(&self, deps: Deps, config: &Config) -> StdResult<Vec<SubMsg>>;
fn on_grid_creation<SIC: SwapInterfaceCollector + Serialize>(
&self,
deps: DepsMut,
env: Env,
info: &MessageInfo,
config: &Config,
position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>>;
fn on_before_swap_execution(
&self,
deps: DepsMut,
config: &Config,
position_id: u64,
) -> StdResult<Vec<CosmosMsg>>;
fn on_after_swap_execution<SIC: SwapInterfaceCollector>(
&self,
deps: DepsMut,
config: &Config,
position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>>;
fn on_remove_grid<SIC: SwapInterfaceCollector>(
&self,
deps: DepsMut,
config: &Config,
position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>>;
fn on_reply<SIC: SwapInterfaceCollector + Serialize>(
&self,
deps: DepsMut,
env: Env,
config: &Config,
reply: Reply,
) -> StdResult<Vec<SubMsg>>;
fn on_query_check(&self, deps: Deps, config: &Config, position_id: u64) -> StdResult<()>;
fn execute_feature<SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned>(
&self,
deps: DepsMut,
env: Env,
info: &MessageInfo,
config: &Config,
feature: Binary,
) -> StdResult<Vec<SubMsg>>;
fn user_info(&self, deps: Deps, user: String, config: &Config)
-> StdResult<Self::UserInfo>;
}
#[cw_serde]
pub struct ActionEmpty {}
impl ActionInterface for ActionEmpty {
type UserInfo = Empty;
fn on_init(&self, _deps: Deps, _config: &Config) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_grid_creation<SIC: SwapInterfaceCollector + Serialize>(
&self,
_deps: DepsMut,
_env: Env,
_info: &MessageInfo,
_config: &Config,
_position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_before_swap_execution(
&self,
_deps: DepsMut,
_config: &Config,
_position_id: u64,
) -> StdResult<Vec<CosmosMsg>> {
Ok(vec![])
}
fn on_after_swap_execution<SIC: SwapInterfaceCollector>(
&self,
_deps: DepsMut,
_config: &Config,
_position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_remove_grid<SIC: SwapInterfaceCollector>(
&self,
_deps: DepsMut,
_config: &Config,
_position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_query_check(
&self,
_deps: Deps,
_config: &Config,
_position_id: u64,
) -> StdResult<()> {
Ok(())
}
fn on_reply<SIC: SwapInterfaceCollector + Serialize>(
&self,
_deps: DepsMut,
_env: Env,
_config: &Config,
_reply: Reply,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn execute_feature<SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned>(
&self,
_deps: DepsMut,
_env: Env,
_info: &MessageInfo,
_config: &Config,
_feature: Binary,
) -> StdResult<Vec<SubMsg>> {
Err(StdError::generic_err("No featue available for ActionEmpty"))
}
fn user_info(
&self,
_deps: Deps,
_user: String,
_config: &Config,
) -> StdResult<Self::UserInfo> {
Ok(Empty {})
}
}
pub mod warp_action_module {
use super::*;
use std::vec;
use cosmwasm_std::{from_json, to_json_binary, Addr, SubMsg};
use cw_storage_plus::Item;
use rhaki_cw_plus::{
traits::{IntoAddr, IntoStdResult},
wasm::CosmosMsgExt,
};
use serde::{de::DeserializeOwned, Serialize};
use warp_controller_pkg::account::QueryAccountMsg;
use crate::{
grid::{
self,
definitions::{ModulesReplyID, USER_POSITIONS},
msgs::TriggerMsg,
},
variable_provider::keys::{WARP_ACCOUNT_CODE_ID, WARP_JOB_FEE_AMOUNT},
};
#[cw_serde]
pub struct ActionWarpAccounts {
pub middle_address: Addr,
pub warp_address: Addr,
}
#[cw_serde]
pub struct WarpRefillFeeInfo {
pub min_amount: Uint128,
pub sell_with: Uint128,
}
#[cw_serde]
pub struct WarpUserInfo {
pub middle_address: Addr,
pub warp_address: Addr,
pub fee_deposited: Uint128,
}
#[cw_serde]
pub struct ActionWarpGridBeingCreated {
pub owner: Addr,
pub fee: Coin,
pub middle_addr: Option<Addr>,
pub position_id: u64,
}
#[cw_serde]
pub enum ActionWarpFeature {
RefillFee {
recreate_job_for_positions_id: Vec<u64>,
},
ExecuteOnWarpAccount(CosmosMsg),
}
pub struct ActionWarp<'a> {
pub warp_accounts: Map<'a, Addr, ActionWarpAccounts>,
pub being_creation: Item<'a, ActionWarpGridBeingCreated>,
}
impl<'a> Default for ActionWarp<'a> {
fn default() -> Self {
Self {
warp_accounts: Map::new("warp_accounts_key"),
being_creation: Item::new("on_init_account_key"),
}
}
}
impl<'a> ActionWarp<'a> {
fn get_warp_fee_denom(&self, deps: Deps, config: &Config) -> StdResult<String> {
let controller = self.get_controller(deps, config)?;
deps.querier
.query_wasm_smart::<warp_controller_pkg::ConfigResponse>(
controller,
&warp_controller_pkg::QueryMsg::QueryConfig(
warp_controller_pkg::QueryConfigMsg {},
),
)
.map(|val| val.config.fee_denom)
}
fn get_controller(&self, deps: Deps, config: &Config) -> StdResult<Addr> {
variable_provider_get_variable(deps, WARP_CONTROLLER, &config.variable_provider)?
.unwrap_addr()
}
fn feature_refill_fee<
SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned,
>(
&self,
deps: DepsMut,
env: Env,
info: &MessageInfo,
config: &Config,
recreate_job_for_positions_id: Vec<u64>,
) -> StdResult<Vec<SubMsg>> {
let fee_denom = self.get_warp_fee_denom(deps.as_ref(), config)?;
let coin =
rhaki_cw_plus::coin::only_one_coin(&info.funds, Some(fee_denom.clone()))?;
let accounts = self.warp_accounts.load(deps.storage, info.sender.clone())?;
let mut msgs = vec![self
.build_refill_fee_msg(accounts.warp_address, fee_denom, coin.amount)?
.into_submsg_never()];
for position_id in recreate_job_for_positions_id {
let position = USER_POSITIONS::<SIC>().load(deps.storage, position_id)?;
if position.owner != info.sender {
return Err(StdError::generic_err("Unauthorized"));
}
let accounts = self.warp_accounts.load(deps.storage, info.sender.clone())?;
msgs.extend(self.on_grid_create_with_account::<SIC>(
deps.as_ref(),
&env,
config,
None,
accounts,
position_id,
)?)
}
Ok(msgs)
}
fn build_refill_fee_msg(
&self,
warp_account: Addr,
fee_denom: String,
amount: Uint128,
) -> StdResult<CosmosMsg> {
Ok(CosmosMsg::Bank(BankMsg::Send {
to_address: warp_account.to_string(),
amount: vec![Coin::new(amount.u128(), fee_denom)],
}))
}
fn on_grid_create_with_account<SIC: SwapInterfaceCollector + Serialize>(
&self,
deps: Deps,
env: &Env,
config: &Config,
fee: Option<Coin>,
accounts: ActionWarpAccounts,
position_id: u64,
) -> StdResult<Vec<SubMsg>> {
let controller = self.get_controller(deps, config)?;
let job_reward = variable_provider_get_variable(
deps,
WARP_JOB_FEE_AMOUNT,
&config.variable_provider,
)?
.unwrap_uint128()?;
let var_name = "action_available";
let msg_create_job = WasmMsg::build_execute(
controller,
warp_controller_pkg::ExecuteMsg::CreateJob(
warp_controller_pkg::job::CreateJobMsg {
name: "grid".to_string(),
description: "grid strategy by big-strats".to_string(),
labels: vec!["grid".to_string()],
condition: rhaki_cw_plus::serde_value::sjw_to_string(
&warp_resolver_pkg::condition::Condition::Expr(Box::new(
warp_resolver_pkg::condition::Expr::Bool(
var_name.into_warp_variable(),
),
)),
)
.into_std_result()?,
terminate_condition: None,
msgs: rhaki_cw_plus::serde_value::sjw_to_string(&vec![
WasmMsg::build_execute(
env.contract.address.clone(),
&grid::msgs::ExecuteMsg::<SIC>::Trigger(TriggerMsg {
id: position_id,
}),
vec![],
)?
.into_cosmos_msg(),
])
.into_std_result()?,
vars: rhaki_cw_plus::serde_value::sjw_to_string(&[
warp_resolver_pkg::variable::Variable::Query(
warp_resolver_pkg::variable::QueryVariable {
kind: warp_resolver_pkg::variable::VariableKind::Bool,
name: var_name.to_string(),
encode: false,
init_fn: warp_resolver_pkg::variable::QueryExpr {
selector: var_name.to_string(),
query: cosmwasm_std::QueryRequest::Wasm(
cosmwasm_std::WasmQuery::Smart {
contract_addr: env.contract.address.to_string(),
msg: to_json_binary(&grid::msgs::QueryMsg::<
SIC,
WarpUserInfo,
>::ActionAvailable {
id: position_id,
})?,
},
),
},
reinitialize: false,
value: None,
update_fn: None,
},
),
])
.into_std_result()?,
recurring: true,
requeue_on_evict: true,
reward: job_reward,
assets_to_withdraw: None,
},
),
vec![],
)?
.into_cosmos_msg();
let mut msgs = vec![];
if let Some(fee) = fee {
if fee.amount > Uint128::zero() {
msgs.push(
CosmosMsg::Bank(BankMsg::Send {
to_address: accounts.warp_address.to_string(),
amount: vec![fee],
})
.into_submsg_never(),
)
}
}
msgs.push(
WasmMsg::build_execute(
accounts.middle_address,
warp_account_pkg::ExecuteMsg::Generic(warp_account_pkg::GenericMsg {
msgs: vec![msg_create_job],
}),
vec![],
)?
.into_cosmos_msg()
.into_submsg_never(),
);
Ok(msgs)
}
fn on_grid_create_without_account(
&self,
deps: DepsMut,
env: Env,
config: &Config,
) -> StdResult<Vec<SubMsg>> {
let code_id = variable_provider_get_variable(
deps.as_ref(),
WARP_ACCOUNT_CODE_ID,
&config.variable_provider,
)?
.unwrap_u64()?;
let msg_init_middle_addr = SubMsg::reply_on_success(
WasmMsg::Instantiate {
admin: Some(env.contract.address.to_string()),
code_id,
msg: to_json_binary(&warp_account_pkg::InstantiateMsg {
owner: env.contract.address.to_string(),
funds: None,
})?,
funds: vec![],
label: "Big strats - gird - mock account".to_string(),
},
ModulesReplyID::InitMiddle.repr(),
);
Ok(vec![msg_init_middle_addr])
}
fn execute_on_warp_account(
&self,
deps: Deps,
owner: Addr,
msg: CosmosMsg,
) -> StdResult<SubMsg> {
let accounts = self.warp_accounts.load(deps.storage, owner)?;
WasmMsg::build_execute(
accounts.middle_address,
warp_account_pkg::ExecuteMsg::Generic(warp_account_pkg::GenericMsg {
msgs: vec![WasmMsg::build_execute(
accounts.warp_address,
warp_account_pkg::ExecuteMsg::Generic(warp_account_pkg::GenericMsg {
msgs: vec![msg],
}),
vec![],
)?
.into_cosmos_msg()],
}),
vec![],
)
.map(|val| val.into_cosmos_msg().into_submsg_never())
}
}
impl<'a> ActionInterface for ActionWarp<'a> {
type UserInfo = WarpUserInfo;
fn on_init(&self, _deps: Deps, _config: &Config) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_grid_creation<SIC: SwapInterfaceCollector + Serialize>(
&self,
deps: DepsMut,
env: Env,
info: &MessageInfo,
config: &Config,
position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
let fee_denom = self.get_warp_fee_denom(deps.as_ref(), config)?;
let mut fee_received =
*rhaki_cw_plus::coin::vec_coins_to_hashmap(info.funds.clone())?
.get(&fee_denom)
.unwrap_or(&Uint128::zero());
if let AssetInfo::Native(val) = &position.base_asset.info {
if *val == fee_denom {
fee_received -= position.base_asset.amount;
}
}
if let AssetInfo::Native(val) = &position.quote_asset.info {
if *val == fee_denom {
fee_received -= position.quote_asset.amount;
}
}
match self.warp_accounts.load(deps.storage, info.sender.clone()) {
Ok(accounts) => {
let fee = if fee_received == Uint128::zero() {
None
} else {
Some(Coin::new(fee_received.u128(), fee_denom))
};
self.on_grid_create_with_account::<SIC>(
deps.as_ref(),
&env,
config,
fee,
accounts,
position.id,
)
}
Err(_) => {
if fee_received == Uint128::zero() {
return Err(StdError::generic_err(
"Fee for warp required for the account",
));
}
let fee = Coin::new(fee_received.u128(), fee_denom);
self.being_creation.save(
deps.storage,
&ActionWarpGridBeingCreated {
owner: info.sender.clone(),
fee,
middle_addr: None,
position_id: position.id,
},
)?;
self.on_grid_create_without_account(deps, env, config)
}
}
}
fn on_before_swap_execution(
&self,
_deps: DepsMut,
_config: &Config,
_position_id: u64,
) -> StdResult<Vec<CosmosMsg>> {
Ok(vec![])
}
fn on_after_swap_execution<SIC: SwapInterfaceCollector>(
&self,
_deps: DepsMut,
_config: &Config,
_position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_remove_grid<SIC: SwapInterfaceCollector>(
&self,
_deps: DepsMut,
_config: &Config,
_position: &UserPosition<SIC>,
) -> StdResult<Vec<SubMsg>> {
Ok(vec![])
}
fn on_query_check(
&self,
_deps: Deps,
_config: &Config,
_position_id: u64,
) -> StdResult<()> {
Ok(())
}
fn on_reply<SIC: SwapInterfaceCollector + Serialize>(
&self,
deps: DepsMut,
env: Env,
config: &Config,
reply: Reply,
) -> StdResult<Vec<SubMsg>> {
match ModulesReplyID::from_repr(reply.id) {
Some(ModulesReplyID::InitMiddle) => {
let middle_address = cw_utils::parse_reply_instantiate_data(reply)
.into_std_result()?
.contract_address
.into_addr(deps.api)?;
let controller = self.get_controller(deps.as_ref(), config)?;
self.being_creation.update(
deps.storage,
|mut val| -> StdResult<ActionWarpGridBeingCreated> {
val.middle_addr = Some(middle_address.clone());
Ok(val)
},
)?;
let msg_create_account = WasmMsg::build_execute(
middle_address,
warp_account_pkg::ExecuteMsg::Generic(warp_account_pkg::GenericMsg {
msgs: vec![WasmMsg::build_execute(
controller,
&warp_controller_pkg::ExecuteMsg::CreateAccount(
warp_controller_pkg::account::CreateAccountMsg {
funds: None,
},
),
vec![],
)?
.into_cosmos_msg()],
}),
vec![],
)?
.into_cosmos_msg()
.into_submsg_on_success(ModulesReplyID::InitWarp.repr(), None);
Ok(vec![msg_create_account])
}
Some(ModulesReplyID::InitWarp) => {
let being_data = self.being_creation.load(deps.storage)?;
let controller = self.get_controller(deps.as_ref(), config)?;
let warp_address = deps
.querier
.query_wasm_smart::<warp_controller_pkg::account::AccountResponse>(
controller,
&warp_controller_pkg::QueryMsg::QueryAccount(QueryAccountMsg {
owner: being_data.middle_addr.clone().unwrap().to_string(),
}),
)?
.account
.account;
let accounts = ActionWarpAccounts {
middle_address: being_data.middle_addr.unwrap(),
warp_address,
};
self.warp_accounts.save(
deps.storage,
being_data.owner.clone(),
&accounts,
)?;
self.on_grid_create_with_account::<SIC>(
deps.as_ref(),
&env,
config,
Some(being_data.fee),
accounts,
being_data.position_id,
)
}
Some(ModulesReplyID::Ignore) => Ok(vec![]),
None => Err(StdError::generic_err(format!(
"Invalid ReplyId for ActionWarp: {}",
reply.id
))),
}
}
fn execute_feature<
SIC: SwapInterfaceCollector + Serialize + Clone + DeserializeOwned,
>(
&self,
deps: DepsMut,
env: Env,
info: &MessageInfo,
config: &Config,
feature: Binary,
) -> StdResult<Vec<SubMsg>> {
match from_json(feature)? {
ActionWarpFeature::RefillFee {
recreate_job_for_positions_id,
} => self.feature_refill_fee::<SIC>(
deps,
env,
info,
config,
recreate_job_for_positions_id,
),
ActionWarpFeature::ExecuteOnWarpAccount(msg) => self
.execute_on_warp_account(deps.as_ref(), info.sender.clone(), msg)
.map(|val| vec![val]),
}
}
fn user_info(
&self,
deps: Deps,
user: String,
config: &Config,
) -> StdResult<Self::UserInfo> {
let accounts = self
.warp_accounts
.load(deps.storage, user.into_addr(deps.api)?)?;
Ok(WarpUserInfo {
middle_address: accounts.middle_address,
fee_deposited: deps
.querier
.query_balance(
&accounts.warp_address,
self.get_warp_fee_denom(deps, config)?,
)?
.amount,
warp_address: accounts.warp_address,
})
}
}
#[allow(clippy::wrong_self_convention)]
pub trait IntoWarpVariable: Into<String> + Clone {
fn into_warp_variable(&self) -> String {
let string: String = self.clone().into();
format!("$warp.variable.{}", string)
}
}
impl IntoWarpVariable for String {}
impl<'a> IntoWarpVariable for &'a str {}
}
}
#[cfg(test)]
mod test {
use cosmwasm_std::Decimal;
use rhaki_cw_plus::math::{IntoDecimal, IntoUint};
#[test]
fn user_position() {
let base_value = "20".into_uint128();
let quote_value = "10".into_uint128();
let upper_bound = "3".into_decimal();
let lower_bound = "1".into_decimal();
let tick_delta = "0.2".into_decimal();
let per = Decimal::from_ratio(base_value, base_value + quote_value);
let interval = ((upper_bound - lower_bound) * per / tick_delta
+ Decimal::from_ratio(1_u128, 2_u128))
.floor()
* tick_delta
+ lower_bound;
assert_eq!(interval, "2.4".into_decimal());
}
}