instantiate/execute/query entry points and every handler behind them. This is where the rules actually run.
Live source from
contracts/oracle/src/contract.rs (293 lines). Generated from the deployed contract code.use cosmwasm_std::{
entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
};
use cw2::set_contract_version;
use crate::error::ContractError;
use crate::msg::{
ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, PriceResponse, QueryMsg,
};
use crate::state::{Config, PricePoint, CONFIG, HISTORY, LATEST};
const CONTRACT_NAME: &str = "crates.io:bwick-oracle";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_HISTORY_SIZE: u32 = 60;
const DEFAULT_TWAP_WINDOW_SECONDS: u64 = 60;
/// Hard ceiling on accepted price values — protects against absurd reports
/// (e.g., if a parsing bug at the relayer ships a huge number). $1 BWICK in micro-USD.
const MAX_ACCEPTED_PRICE: u128 = 1_000_000;
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let relayer = deps.api.addr_validate(&msg.relayer_address)?;
let history_size = msg.history_size.unwrap_or(DEFAULT_HISTORY_SIZE).max(1);
let config = Config {
admin: info.sender.clone(),
relayer_address: relayer.clone(),
max_age_seconds: msg.max_age_seconds,
history_size,
};
CONFIG.save(deps.storage, &config)?;
HISTORY.save(deps.storage, &Vec::new())?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("admin", info.sender)
.add_attribute("relayer", relayer)
.add_attribute("max_age_seconds", msg.max_age_seconds.to_string())
.add_attribute("history_size", history_size.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::new().add_attribute("action", "migrate"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::UpdatePrice {
bwick_usd_price,
sol_usd_price,
source,
} => execute_update_price(deps, env, info, bwick_usd_price, sol_usd_price, source),
ExecuteMsg::UpdateRelayer { relayer_address } => {
execute_update_relayer(deps, info, relayer_address)
}
ExecuteMsg::UpdateMaxAge { max_age_seconds } => {
execute_update_max_age(deps, info, max_age_seconds)
}
}
}
fn execute_update_price(
deps: DepsMut,
env: Env,
info: MessageInfo,
bwick_usd_price: Uint128,
sol_usd_price: Uint128,
source: String,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.relayer_address {
return Err(ContractError::Unauthorized {});
}
let bwick = bwick_usd_price.u128();
let sol = sol_usd_price.u128();
if bwick == 0 {
return Err(ContractError::InvalidPrice {
reason: "bwick_usd_price must be positive".into(),
});
}
if bwick > MAX_ACCEPTED_PRICE {
return Err(ContractError::InvalidPrice {
reason: format!(
"bwick_usd_price {} exceeds ceiling {}",
bwick, MAX_ACCEPTED_PRICE
),
});
}
let point = PricePoint {
bwick_usd_price: bwick,
sol_usd_price: sol,
source: source.clone(),
height: env.block.height,
timestamp: env.block.time.seconds(),
};
LATEST.save(deps.storage, &point)?;
// Append to history, trim to history_size.
let mut hist = HISTORY.load(deps.storage)?;
hist.push(point);
let limit = config.history_size as usize;
if hist.len() > limit {
let drop = hist.len() - limit;
hist.drain(0..drop);
}
HISTORY.save(deps.storage, &hist)?;
// Touch config (to keep migration semantics simple; not strictly needed).
CONFIG.save(deps.storage, &mut config)?;
Ok(Response::new()
.add_attribute("action", "update_price")
.add_attribute("bwick_usd_price", bwick.to_string())
.add_attribute("sol_usd_price", sol.to_string())
.add_attribute("source", source)
.add_attribute("height", env.block.height.to_string()))
}
fn execute_update_relayer(
deps: DepsMut,
info: MessageInfo,
relayer_address: String,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
let new_relayer = deps.api.addr_validate(&relayer_address)?;
config.relayer_address = new_relayer.clone();
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_relayer")
.add_attribute("relayer", new_relayer))
}
fn execute_update_max_age(
deps: DepsMut,
info: MessageInfo,
max_age_seconds: u64,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
config.max_age_seconds = max_age_seconds;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new()
.add_attribute("action", "update_max_age")
.add_attribute("max_age_seconds", max_age_seconds.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Price { require_fresh } => to_binary(&query_price(deps, env, require_fresh)?),
QueryMsg::Twap { window_seconds } => to_binary(&query_twap(deps, env, window_seconds)?),
QueryMsg::Config {} => to_binary(&query_config(deps)?),
}
}
fn query_price(deps: Deps, env: Env, require_fresh: Option<bool>) -> StdResult<PriceResponse> {
let config = CONFIG.load(deps.storage)?;
let latest = LATEST.may_load(deps.storage)?;
let p = match latest {
Some(p) => p,
None => {
return Err(cosmwasm_std::StdError::generic_err("no price data yet"));
}
};
let now = env.block.time.seconds();
let fresh = now.saturating_sub(p.timestamp) <= config.max_age_seconds;
if require_fresh.unwrap_or(false) && !fresh {
return Err(cosmwasm_std::StdError::generic_err(format!(
"price stale: last update {}s ago, max_age {}s",
now.saturating_sub(p.timestamp),
config.max_age_seconds
)));
}
Ok(PriceResponse {
bwick_usd_price: Uint128::from(p.bwick_usd_price),
sol_usd_price: Uint128::from(p.sol_usd_price),
last_update_height: p.height,
last_update_timestamp: p.timestamp,
source: p.source,
fresh,
})
}
fn query_twap(deps: Deps, env: Env, window_seconds: Option<u64>) -> StdResult<PriceResponse> {
let config = CONFIG.load(deps.storage)?;
let hist = HISTORY.load(deps.storage)?;
let latest = LATEST.may_load(deps.storage)?;
let now = env.block.time.seconds();
let window = window_seconds.unwrap_or(DEFAULT_TWAP_WINDOW_SECONDS);
let cutoff = now.saturating_sub(window);
// Filter history to within the window.
let in_window: Vec<&PricePoint> = hist.iter().filter(|p| p.timestamp >= cutoff).collect();
// No history in window → fall back to spot.
if in_window.is_empty() {
match latest {
Some(p) => {
let fresh = now.saturating_sub(p.timestamp) <= config.max_age_seconds;
return Ok(PriceResponse {
bwick_usd_price: Uint128::from(p.bwick_usd_price),
sol_usd_price: Uint128::from(p.sol_usd_price),
last_update_height: p.height,
last_update_timestamp: p.timestamp,
source: p.source,
fresh,
});
}
None => {
return Err(cosmwasm_std::StdError::generic_err("no price data yet"));
}
}
}
// Compute time-weighted average. Each point's weight = duration to next point
// (or duration to `now` for the last one).
let mut total_bwick: u128 = 0;
let mut total_sol: u128 = 0;
let mut total_weight: u128 = 0;
for (i, p) in in_window.iter().enumerate() {
let next_ts = if i + 1 < in_window.len() {
in_window[i + 1].timestamp
} else {
now
};
let weight = next_ts.saturating_sub(p.timestamp).max(1) as u128;
total_bwick = total_bwick.saturating_add(p.bwick_usd_price.saturating_mul(weight));
total_sol = total_sol.saturating_add(p.sol_usd_price.saturating_mul(weight));
total_weight = total_weight.saturating_add(weight);
}
let twap_bwick = if total_weight > 0 {
total_bwick / total_weight
} else {
in_window[0].bwick_usd_price
};
let twap_sol = if total_weight > 0 {
total_sol / total_weight
} else {
in_window[0].sol_usd_price
};
let last = in_window[in_window.len() - 1];
let fresh = now.saturating_sub(last.timestamp) <= config.max_age_seconds;
Ok(PriceResponse {
bwick_usd_price: Uint128::from(twap_bwick),
sol_usd_price: Uint128::from(twap_sol),
last_update_height: last.height,
last_update_timestamp: last.timestamp,
source: format!("twap-{}s", window),
fresh,
})
}
fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
let hist = HISTORY.load(deps.storage)?;
Ok(ConfigResponse {
admin: config.admin,
relayer_address: config.relayer_address,
max_age_seconds: config.max_age_seconds,
history_size: config.history_size,
history_count: hist.len() as u32,
})
}

