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. Trigger.rs handles both ABI encoded data for trigger and submission data and raw data for local testing. 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;/// Represents the destination where the trigger output should be sentpub enum Destination {Ethereum,CliOutput,}/// Decodes incoming trigger event data into its components/// Handles two types of triggers:/// 1. EvmContractEvent - Decodes Ethereum event logs using the NewTrigger ABI/// 2. Raw - Used for direct CLI testing with no encodingpub 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 as SolValue>::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")),}}/// Encodes the output data for submission back to Ethereumpub 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,}}/// The `sol!` macro from alloy_sol_macro reads a Solidity interface file/// and generates corresponding Rust types and encoding/decoding functions.pub mod solidity {use alloy_sol_macro::sol;pub use ITypes::*;// The objects here will be generated automatically into Rust types.sol!("../../src/interfaces/ITypes.sol");// Encode string input from the trigger contract functionsol! {function addTrigger(string data) external;}}
Visit the Blockchain interactions page for more information on the sol!
macro and how to use it to generate Rust types from Solidity interfaces.
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::{evm::alloy_primitives::hex,http::{fetch_json, http_request_get},};pub mod bindings;use crate::bindings::{export, Guest, TriggerAction, WasmResponse};use alloy_sol_types::SolValue;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 submitted to the blockchain, 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 (useful 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())?;let hex_data = match String::from_utf8(req.clone()) {Ok(input_str) if input_str.starts_with("0x") => {// Local testing: hex string inputhex::decode(&input_str[2..]).map_err(|e| format!("Failed to decode hex string: {}", e))?}_ => {// Production: direct binary ABI inputreq.clone()}};let decoded = <String as SolValue>::abi_decode(&hex_data).map_err(|e| format!("Failed to decode ABI string: {}", e))?;let id =decoded.trim().parse::<u64>().map_err(|_| format!("Invalid number: {}", decoded))?;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.