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

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;
/// Represents the destination where the trigger output should be sent
pub 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 encoding
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 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 Ethereum
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,
}
}
/// 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 function
sol! {
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.

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

  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())?;
let hex_data = match String::from_utf8(req.clone()) {
Ok(input_str) if input_str.starts_with("0x") => {
// Local testing: hex string input
hex::decode(&input_str[2..])
.map_err(|e| format!("Failed to decode hex string: {}", e))?
}
_ => {
// Production: direct binary ABI input
req.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.

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