4. Oracle component walkthrough
The core logic of the price oracle in this example is located in the /evm-price-oracle/src/lib.rs
file. Scroll down to follow a walkthrough of the code for the oracle component.
trigger.rs
The trigger.rs file handles the decoding of incoming trigger data from the trigger event emitted by the trigger contract. The component uses decode_event_log_data!()
from the wavs-wasi-utils crate to decode the event log data and prepares it for processing within the WAVS component. For more information on different trigger types, visit the Triggers page. To learn more about trigger input handling, visit the Component page.
use crate::bindings::wavs::worker::layer_types::{TriggerData, TriggerDataEvmContractEvent, WasmResponse,};use alloy_sol_types::SolValue;use anyhow::Result;use wavs_wasi_utils::decode_event_log_data;pub enum Destination {Ethereum,CliOutput,}pub fn decode_trigger_event(trigger_data: TriggerData) -> Result<(u64, Vec<u8>, Destination)> {match trigger_data {TriggerData::EvmContractEvent(TriggerDataEvmContractEvent { log, .. }) => {let event: solidity::NewTrigger = decode_event_log_data!(log)?;let trigger_info = solidity::TriggerInfo::abi_decode(&event._triggerInfo)?;Ok((trigger_info.triggerId, trigger_info.data.to_vec(), Destination::Ethereum))}TriggerData::Raw(data) => Ok((0, data.clone(), Destination::CliOutput)),_ => Err(anyhow::anyhow!("Unsupported trigger data type")),}}pub fn encode_trigger_output(trigger_id: u64, output: impl AsRef<[u8]>) -> WasmResponse {WasmResponse {payload: solidity::DataWithId {triggerId: trigger_id,data: output.as_ref().to_vec().into(),}.abi_encode(),ordering: None,}}mod solidity {use alloy_sol_macro::sol;pub use ITypes::*;sol!("../../src/interfaces/ITypes.sol");}
Oracle component logic
The lib.rs
file contains the main component logic for the oracle. The first section of the code imports the required modules for requests, serialization, and bindings, defines the component struct, and exports the component for execution within the WAVS runtime.
mod trigger;use trigger::{decode_trigger_event, encode_trigger_output, Destination};use wavs_wasi_utils::http::{fetch_json, http_request_get};pub mod bindings;use crate::bindings::{export, Guest, TriggerAction, WasmResponse};use serde::{Deserialize, Serialize};use wstd::{http::HeaderValue, runtime::block_on};
The run
function is the main entry point for the price oracle component. WAVS is subscribed to watch for events emitted by the blockchain. When WAVS observes an event is emitted, it will internally route the event and its data to this function (component). The processing then occurs before the output is returned back to WAVS to be submitted to the blockchain by the operator(s).
This is why the Destination::Ethereum
requires the encoded trigger output, it must be ABI encoded for the solidity contract.
After the data is properly set by the operator through WAVS, any user can query the price data from the blockchain in the solidity contract. You can also return None
as the output if nothing needs to be saved to the blockchain. (great for performing some off chain action)
The run
function:
- Receives a trigger action containing encoded data
- Decodes the input to get a cryptocurrency ID (in hex)
- Fetches current price data from CoinMarketCap
- Returns the encoded response based on the destination
struct Component;export!(Component with_types_in bindings);impl Guest for Component {fn run(action: TriggerAction) -> std::result::Result<Option<WasmResponse>, String> {let (trigger_id, req, dest) =decode_trigger_event(action.data).map_err(|e| e.to_string())?;// Convert bytes to string and parse first char as u64let input = std::str::from_utf8(&req).map_err(|e| e.to_string())?;println!("input id: {}", input);let id = input.chars().next().ok_or("Empty input")?;let id = id.to_digit(16).ok_or("Invalid hex digit")? as u64;let res = block_on(async move {let resp_data = get_price_feed(id).await?;println!("resp_data: {:?}", resp_data);serde_json::to_vec(&resp_data).map_err(|e| e.to_string())})?;let output = match dest {Destination::Ethereum => Some(encode_trigger_output(trigger_id, &res)),Destination::CliOutput => Some(WasmResponse { payload: res.into(), ordering: None }),};Ok(output)}}
Visit the Component page for more information on the run
function and the main requirements for component structure.
Fetching price data
The get_price_feed
function is responsible for fetching price data from CoinMarketCap's API. It takes the cryptocurrency ID passed from the trigger as input and returns a structured PriceFeedData
containing the symbol, current price in USD, and server timestamp. For more information on making network requests in WAVS components, visit the Network Requests page.
async fn get_price_feed(id: u64) -> Result<PriceFeedData, String> {let url = format!("https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail?id={}&range=1h",id);let current_time = std::time::SystemTime::now().elapsed().unwrap().as_secs();let mut req = http_request_get(&url).map_err(|e| e.to_string())?;req.headers_mut().insert("Accept", HeaderValue::from_static("application/json"));req.headers_mut().insert("Content-Type", HeaderValue::from_static("application/json"));req.headers_mut().insert("User-Agent", HeaderValue::from_static("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"));req.headers_mut().insert("Cookie",HeaderValue::from_str(&format!("myrandom_cookie={}", current_time)).unwrap(),);let json: Root = fetch_json(req).await.map_err(|e| e.to_string())?;// round to the nearest 3 decimal placeslet price = (json.data.statistics.price * 100.0).round() / 100.0;// timestamp is 2025-04-30T19:59:44.161Z, becomes 2025-04-30T19:59:44let timestamp = json.status.timestamp.split('.').next().unwrap_or("");Ok(PriceFeedData { symbol: json.data.symbol, price, timestamp: timestamp.to_string() })}
Handling the response
The processed price data is returned as a WasmResponse
which contains the response payload. The response is formatted differently based on the destination.
let output = match dest {Destination::Ethereum => Some(encode_trigger_output(trigger_id, &res)),Destination::CliOutput => Some(WasmResponse { payload: res.into(), ordering: None }),};Ok(output)
- For
Destination::CliOutput
, the raw data is returned directly for local testing and debugging using thewasi-exec
command. - For
Destination::Ethereum
, the data is ABI encoded usingencode_trigger_output
. This ensures that processed data is formatted correctly before being sent to the submission contract.
In trigger.rs
, the WasmResponse
struct is used to standardize the format of data returned from components. The payload
field contains the processed data from the component.
pub fn encode_trigger_output(trigger_id: u64, output: impl AsRef<[u8]>) -> WasmResponse {WasmResponse {payload: solidity::DataWithId {triggerId: trigger_id,data: output.as_ref().to_vec().into(),}.abi_encode(),ordering: None,}}
For more information on component outputs, visit the Component page. To learn more about submission logic, visit the Submission page.
The Service handbook contains more detailed information on each part of developing services and components. Visit the Service handbook overview to learn more.
Next steps
Continue to the next section to learn how to build and test your component.