-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #195 from decentrio/vuong/band-oracle
feat: band price feeder
- Loading branch information
Showing
28 changed files
with
880 additions
and
195 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
[package] | ||
name = "mesh-band-price-feed" | ||
description = "Returns exchange rates of assets fetched from Band Protocol" | ||
version = { workspace = true } | ||
edition = { workspace = true } | ||
license = { workspace = true } | ||
repository = { workspace = true } | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
[lib] | ||
crate-type = ["cdylib", "rlib"] | ||
|
||
[features] | ||
# for more explicit tests, cargo test --features=backtraces | ||
backtraces = ["cosmwasm-std/backtraces"] | ||
# use library feature to disable all instantiate/execute/query exports | ||
library = [] | ||
# enables generation of mt utilities | ||
mt = ["library", "sylvia/mt"] | ||
|
||
|
||
[dependencies] | ||
mesh-apis = { workspace = true } | ||
mesh-price-feed = { workspace = true } | ||
|
||
sylvia = { workspace = true } | ||
cosmwasm-schema = { workspace = true } | ||
cosmwasm-std = { workspace = true } | ||
cw-storage-plus = { workspace = true } | ||
cw2 = { workspace = true } | ||
cw-utils = { workspace = true } | ||
|
||
schemars = { workspace = true } | ||
serde = { workspace = true } | ||
thiserror = { workspace = true } | ||
obi = { workspace = true } | ||
cw-band = { workspace = true } | ||
|
||
[dev-dependencies] | ||
cw-multi-test = { workspace = true } | ||
test-case = { workspace = true } | ||
derivative = { workspace = true } | ||
anyhow = { workspace = true } | ||
|
||
[[bin]] | ||
name = "schema" | ||
doc = false |
2 changes: 1 addition & 1 deletion
2
...sumer/remote-price-feed/src/bin/schema.rs → ...onsumer/band-price-feed/src/bin/schema.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
use cosmwasm_std::{ | ||
to_json_binary, Binary, Coin, DepsMut, Env, IbcChannel, IbcMsg, IbcTimeout, Response, Uint64, | ||
}; | ||
use cw2::set_contract_version; | ||
use cw_storage_plus::Item; | ||
use cw_utils::nonpayable; | ||
use mesh_apis::price_feed_api::{PriceFeedApi, PriceResponse}; | ||
|
||
use crate::error::ContractError; | ||
use crate::state::{Config, TradingPair}; | ||
|
||
use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, SudoCtx}; | ||
use sylvia::{contract, schemars}; | ||
|
||
use cw_band::{Input, OracleRequestPacketData}; | ||
use mesh_price_feed::{Action, PriceKeeper, Scheduler}; | ||
use obi::enc::OBIEncode; | ||
|
||
// Version info for migration | ||
const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); | ||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); | ||
|
||
pub struct RemotePriceFeedContract { | ||
pub channel: Item<'static, IbcChannel>, | ||
pub config: Item<'static, Config>, | ||
pub trading_pair: Item<'static, TradingPair>, | ||
pub price_keeper: PriceKeeper, | ||
pub scheduler: Scheduler<Box<dyn Action<ContractError>>, ContractError>, | ||
} | ||
|
||
impl Default for RemotePriceFeedContract { | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
} | ||
|
||
#[cfg_attr(not(feature = "library"), sylvia::entry_points)] | ||
#[contract] | ||
#[sv::error(ContractError)] | ||
#[sv::messages(mesh_apis::price_feed_api as PriceFeedApi)] | ||
impl RemotePriceFeedContract { | ||
pub fn new() -> Self { | ||
Self { | ||
channel: Item::new("channel"), | ||
config: Item::new("config"), | ||
trading_pair: Item::new("tpair"), | ||
price_keeper: PriceKeeper::new(), | ||
// TODO: the indirection can be removed once Sylvia supports | ||
// generics. The constructor can then probably be constant. | ||
// | ||
// Stable existential types would be even better! | ||
// https://github.com/rust-lang/rust/issues/63063 | ||
scheduler: Scheduler::new(Box::new(try_request)), | ||
} | ||
} | ||
|
||
#[sv::msg(instantiate)] | ||
pub fn instantiate( | ||
&self, | ||
mut ctx: InstantiateCtx, | ||
trading_pair: TradingPair, | ||
client_id: String, | ||
oracle_script_id: Uint64, | ||
ask_count: Uint64, | ||
min_count: Uint64, | ||
fee_limit: Vec<Coin>, | ||
prepare_gas: Uint64, | ||
execute_gas: Uint64, | ||
minimum_sources: u8, | ||
price_info_ttl_in_secs: u64, | ||
) -> Result<Response, ContractError> { | ||
nonpayable(&ctx.info)?; | ||
|
||
set_contract_version(ctx.deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; | ||
self.trading_pair.save(ctx.deps.storage, &trading_pair)?; | ||
self.config.save( | ||
ctx.deps.storage, | ||
&Config { | ||
client_id, | ||
oracle_script_id, | ||
ask_count, | ||
min_count, | ||
fee_limit, | ||
prepare_gas, | ||
execute_gas, | ||
minimum_sources, | ||
}, | ||
)?; | ||
self.price_keeper | ||
.init(&mut ctx.deps, price_info_ttl_in_secs)?; | ||
Ok(Response::new()) | ||
} | ||
|
||
#[sv::msg(exec)] | ||
pub fn request(&self, ctx: ExecCtx) -> Result<Response, ContractError> { | ||
let ExecCtx { deps, env, info: _ } = ctx; | ||
try_request(deps, &env) | ||
} | ||
} | ||
|
||
impl PriceFeedApi for RemotePriceFeedContract { | ||
type Error = ContractError; | ||
// FIXME: make these under a feature flag if we need virtual-staking multitest compatibility | ||
type ExecC = cosmwasm_std::Empty; | ||
type QueryC = cosmwasm_std::Empty; | ||
|
||
/// Return the price of the foreign token. That is, how many native tokens | ||
/// are needed to buy one foreign token. | ||
fn price(&self, ctx: QueryCtx) -> Result<PriceResponse, Self::Error> { | ||
Ok(self | ||
.price_keeper | ||
.price(ctx.deps, &ctx.env) | ||
.map(|rate| PriceResponse { | ||
native_per_foreign: rate, | ||
})?) | ||
} | ||
|
||
fn handle_epoch(&self, ctx: SudoCtx) -> Result<Response, Self::Error> { | ||
self.scheduler.trigger(ctx.deps, &ctx.env) | ||
} | ||
} | ||
|
||
// TODO: Possible features | ||
// - Request fee + Bounty logic to prevent request spam and incentivize relayer | ||
// - Whitelist who can call update price | ||
pub fn try_request(deps: DepsMut, env: &Env) -> Result<Response, ContractError> { | ||
let contract = RemotePriceFeedContract::new(); | ||
let TradingPair { | ||
base_asset, | ||
quote_asset, | ||
} = contract.trading_pair.load(deps.storage)?; | ||
let config = contract.config.load(deps.storage)?; | ||
let channel = contract | ||
.channel | ||
.may_load(deps.storage)? | ||
.ok_or(ContractError::IbcChannelNotOpen)?; | ||
|
||
let raw_calldata = Input { | ||
symbols: vec![base_asset, quote_asset], | ||
minimum_sources: config.minimum_sources, | ||
} | ||
.try_to_vec() | ||
.map(Binary) | ||
.map_err(|err| ContractError::CustomError { | ||
val: err.to_string(), | ||
})?; | ||
|
||
let packet = OracleRequestPacketData { | ||
client_id: config.client_id, | ||
oracle_script_id: config.oracle_script_id, | ||
calldata: raw_calldata, | ||
ask_count: config.ask_count, | ||
min_count: config.min_count, | ||
prepare_gas: config.prepare_gas, | ||
execute_gas: config.execute_gas, | ||
fee_limit: config.fee_limit, | ||
}; | ||
|
||
Ok(Response::new().add_message(IbcMsg::SendPacket { | ||
channel_id: channel.endpoint.channel_id, | ||
data: to_json_binary(&packet)?, | ||
timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(60)), | ||
})) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use cosmwasm_std::{ | ||
testing::{mock_dependencies, mock_env, mock_info}, | ||
Uint128, Uint64, | ||
}; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn instantiation() { | ||
let mut deps = mock_dependencies(); | ||
let env = mock_env(); | ||
let info = mock_info("sender", &[]); | ||
let contract = RemotePriceFeedContract::new(); | ||
|
||
let trading_pair = TradingPair { | ||
base_asset: "base".to_string(), | ||
quote_asset: "quote".to_string(), | ||
}; | ||
|
||
contract | ||
.instantiate( | ||
InstantiateCtx { | ||
deps: deps.as_mut(), | ||
env, | ||
info, | ||
}, | ||
trading_pair, | ||
"07-tendermint-0".to_string(), | ||
Uint64::new(1), | ||
Uint64::new(10), | ||
Uint64::new(50), | ||
vec![Coin { | ||
denom: "uband".to_string(), | ||
amount: Uint128::new(1), | ||
}], | ||
Uint64::new(100000), | ||
Uint64::new(200000), | ||
1, | ||
60, | ||
) | ||
.unwrap(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
use cosmwasm_std::StdError; | ||
use cw_utils::PaymentError; | ||
use thiserror::Error; | ||
|
||
use mesh_price_feed::PriceKeeperError; | ||
|
||
/// Never is a placeholder to ensure we don't return any errors | ||
#[derive(Error, Debug)] | ||
pub enum Never {} | ||
|
||
#[derive(Error, Debug, PartialEq)] | ||
pub enum ContractError { | ||
#[error("{0}")] | ||
Std(#[from] StdError), | ||
|
||
#[error("{0}")] | ||
Payment(#[from] PaymentError), | ||
|
||
#[error("{0}")] | ||
PriceKeeper(#[from] PriceKeeperError), | ||
|
||
#[error("Unauthorized")] | ||
Unauthorized, | ||
|
||
#[error("Request didn't suceess")] | ||
RequestNotSuccess {}, | ||
|
||
#[error("Only supports channel with ibc version bandchain-1, got {version}")] | ||
InvalidIbcVersion { version: String }, | ||
|
||
#[error("Only supports unordered channel")] | ||
OnlyUnorderedChannel {}, | ||
|
||
#[error("The provided IBC channel is not open")] | ||
IbcChannelNotOpen, | ||
|
||
#[error("Contract already has an open IBC channel")] | ||
IbcChannelAlreadyOpen, | ||
|
||
#[error("You must start the channel handshake on the other side, it doesn't support OpenInit")] | ||
IbcOpenInitDisallowed, | ||
|
||
#[error("Contract does not receive packets ack")] | ||
IbcAckNotAccepted, | ||
|
||
#[error("Contract does not receive packets timeout")] | ||
IbcTimeoutNotAccepted, | ||
|
||
#[error("Response packet should only contains 2 symbols")] | ||
InvalidResponsePacket, | ||
|
||
#[error("Symbol must be base denom or quote denom")] | ||
SymbolsNotMatch, | ||
|
||
#[error("Invalid price, must be greater than 0.0")] | ||
InvalidPrice, | ||
|
||
#[error("Custom Error val: {val:?}")] | ||
CustomError { val: String }, | ||
// Add any other custom errors you like here. | ||
// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. | ||
} |
Oops, something went wrong.