Layer LogoWAVS Docs
Build a service

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.

trigger.rs
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.

lib.rs
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:

  1. Receives a trigger action containing encoded data
  2. Decodes the input to get a cryptocurrency ID (in hex)
  3. Fetches current price data from CoinMarketCap
  4. Returns the encoded response based on the destination
lib.rs
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 u64
let 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.

lib.rs
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 places
let price = (json.data.statistics.price * 100.0).round() / 100.0;
// timestamp is 2025-04-30T19:59:44.161Z, becomes 2025-04-30T19:59:44
let 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.

lib.rs
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 the wasi-exec command.
  • For Destination::Ethereum, the data is ABI encoded using encode_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.

trigger.rs
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.

Edit on GitHub

On this page