pyth-crosschain/target_chains/cosmwasm/examples/cw-contract/src/contract.rs

291 lines
10 KiB
Rust

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use {
crate::{
msg::{
ExecuteMsg,
FetchPriceResponse,
InstantiateMsg,
MigrateMsg,
QueryMsg,
},
state::{
State,
STATE,
},
},
cosmwasm_std::{
to_binary,
Binary,
Coin,
Deps,
DepsMut,
Env,
MessageInfo,
Response,
StdError,
StdResult,
},
pyth_sdk_cw::{
get_update_fee,
get_valid_time_period,
query_price_feed,
PriceFeedResponse,
},
std::time::Duration,
};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::new().add_attribute("method", "migrate"))
}
/// The instantiate function is invoked when the contract is first deployed.
/// This function sets configuration values that are used by the query function.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
// It is a good practice that your contract stores the pyth contract address and ids of the
// price feeds it needs upon instantiation or by an authorized approach. This will ensure
// that a wrong address won't be used.
let state = State {
pyth_contract_addr: deps.api.addr_validate(msg.pyth_contract_addr.as_ref())?,
price_feed_id: msg.price_feed_id,
};
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("method", "instantiate")
.add_attribute("price_id", format!("{}", msg.price_feed_id)))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: ExecuteMsg,
) -> StdResult<Response> {
Ok(Response::new().add_attribute("method", "execute"))
}
/// Query the Pyth contract the current price of the configured price feed.
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::FetchPrice {} => to_binary(&query_fetch_price(deps, env)?),
QueryMsg::FetchUpdateFee { vaas } => to_binary(&query_fetch_update_fee(deps, vaas)?),
QueryMsg::FetchValidTimePeriod => to_binary(&query_fetch_valid_time_period(deps)?),
}
}
fn query_fetch_price(deps: Deps, env: Env) -> StdResult<FetchPriceResponse> {
let state = STATE.load(deps.storage)?;
// query_price_feed is the standard way to read the current price from a Pyth price feed.
// It takes the address of the Pyth contract (which is fixed for each network) and the id of the
// price feed. The result is a PriceFeed object with fields for the current price and other
// useful information. The function will fail if the contract address or price feed id are
// invalid.
let price_feed_response: PriceFeedResponse =
query_price_feed(&deps.querier, state.pyth_contract_addr, state.price_feed_id)?;
let price_feed = price_feed_response.price_feed;
// Get the current price and confidence interval from the price feed.
// This function returns None if the price is not currently available.
// This condition can happen for various reasons. For example, some products only trade at
// specific times, or network outages may prevent the price feed from updating.
//
// The example code below throws an error if the price is not available. It is recommended that
// you handle this scenario more carefully. Consult the [consumer best practices](https://docs.pyth.network/documentation/pythnet-price-feeds/best-practices)
// for recommendations.
let current_price = price_feed
.get_price_no_older_than(env.block.time.seconds() as i64, 60)
.ok_or_else(|| StdError::not_found("Current price is not available"))?;
// Get an exponentially-weighted moving average price and confidence interval.
// The same notes about availability apply to this price.
let ema_price = price_feed
.get_ema_price_no_older_than(env.block.time.seconds() as i64, 60)
.ok_or_else(|| StdError::not_found("EMA price is not available"))?;
Ok(FetchPriceResponse {
current_price,
ema_price,
})
}
fn query_fetch_update_fee(deps: Deps, vaas: Vec<Binary>) -> StdResult<Coin> {
let state = STATE.load(deps.storage)?;
let coin = get_update_fee(&deps.querier, state.pyth_contract_addr, vaas.as_slice())?;
Ok(coin)
}
fn query_fetch_valid_time_period(deps: Deps) -> StdResult<Duration> {
let state = STATE.load(deps.storage)?;
let duration = get_valid_time_period(&deps.querier, state.pyth_contract_addr)?;
Ok(duration)
}
#[cfg(test)]
mod test {
use {
super::*,
cosmwasm_std::{
from_binary,
testing::{
mock_dependencies,
mock_env,
MockApi,
MockQuerier,
MockStorage,
},
Addr,
Coin,
OwnedDeps,
QuerierResult,
SystemError,
SystemResult,
Timestamp,
WasmQuery,
},
pyth_sdk_cw::{
testing::MockPyth,
Price,
PriceFeed,
PriceIdentifier,
UnixTimestamp,
},
std::{
convert::TryFrom,
time::Duration,
},
};
// Dummy contract address for testing.
// For real deployments, see list of contract addresses here https://docs.pyth.network/documentation/pythnet-price-feeds/cosmwasm
const PYTH_CONTRACT_ADDR: &str = "pyth_contract_addr";
// For real deployments, see list of price feed ids here https://pyth.network/developers/price-feed-ids
const PRICE_ID: &str = "63f341689d98a12ef60a5cff1d7f85c70a9e17bf1575f0e7c0b2512d48b1c8b3";
fn default_state() -> State {
State {
pyth_contract_addr: Addr::unchecked(PYTH_CONTRACT_ADDR),
price_feed_id: PriceIdentifier::from_hex(PRICE_ID).unwrap(),
}
}
fn setup_test(
state: &State,
mock_pyth: &MockPyth,
block_timestamp: UnixTimestamp,
) -> (OwnedDeps<MockStorage, MockApi, MockQuerier>, Env) {
let mut dependencies = mock_dependencies();
let mock_pyth_copy = (*mock_pyth).clone();
dependencies
.querier
.update_wasm(move |x| handle_wasm_query(&mock_pyth_copy, x));
STATE.save(dependencies.as_mut().storage, state).unwrap();
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(u64::try_from(block_timestamp).unwrap());
(dependencies, env)
}
// Create a handler like this in your test to handle pyth queries. If needed, other contracts
// can be configured in this handler via additional cases.
fn handle_wasm_query(pyth: &MockPyth, wasm_query: &WasmQuery) -> QuerierResult {
match wasm_query {
WasmQuery::Smart { contract_addr, msg } if *contract_addr == PYTH_CONTRACT_ADDR => {
pyth.handle_wasm_query(msg)
}
WasmQuery::Smart { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
WasmQuery::Raw { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
WasmQuery::ContractInfo { contract_addr, .. } => {
SystemResult::Err(SystemError::NoSuchContract {
addr: contract_addr.clone(),
})
}
_ => unreachable!(),
}
}
#[test]
fn test_get_price() {
// Arbitrary unix timestamp to coordinate the price feed timestamp and the block time.
let current_unix_time = 10_000_000;
let mut mock_pyth = MockPyth::new(Duration::from_secs(60), Coin::new(1, "foo"), &[]);
let price_feed = PriceFeed::new(
PriceIdentifier::from_hex(PRICE_ID).unwrap(),
Price {
price: 100,
conf: 10,
expo: -1,
publish_time: current_unix_time,
},
Price {
price: 200,
conf: 20,
expo: -1,
publish_time: current_unix_time,
},
);
mock_pyth.add_feed(price_feed);
let (deps, env) = setup_test(&default_state(), &mock_pyth, current_unix_time);
let msg = QueryMsg::FetchPrice {};
let result = query(deps.as_ref(), env, msg)
.and_then(|binary| from_binary::<FetchPriceResponse>(&binary));
assert_eq!(result.map(|r| r.current_price.price), Ok(100));
}
#[test]
fn test_query_fetch_valid_time_period() {
// Arbitrary unix timestamp to coordinate the price feed timestamp and the block time.
let current_unix_time = 10_000_000;
let mock_pyth = MockPyth::new(Duration::from_secs(60), Coin::new(1, "foo"), &[]);
let (deps, env) = setup_test(&default_state(), &mock_pyth, current_unix_time);
let msg = QueryMsg::FetchValidTimePeriod {};
let result =
query(deps.as_ref(), env, msg).and_then(|binary| from_binary::<Duration>(&binary));
assert_eq!(result.map(|r| r.as_secs()), Ok(60));
}
#[test]
fn test_query_fetch_update_fee() {
// Arbitrary unix timestamp to coordinate the price feed timestamp and the block time.
let current_unix_time = 10_000_000;
let mock_pyth = MockPyth::new(Duration::from_secs(60), Coin::new(1, "foo"), &[]);
let (deps, env) = setup_test(&default_state(), &mock_pyth, current_unix_time);
let msg = QueryMsg::FetchUpdateFee {
vaas: vec![Binary(vec![1, 2, 3])],
};
let result = query(deps.as_ref(), env, msg).and_then(|binary| from_binary::<Coin>(&binary));
assert_eq!(result.map(|r| r.to_string()), Ok(String::from("1foo")))
}
}