Component overview
Service components contain the main business logic of a WAVS service.
Component languages
WAVS enables developers to write components in different programming languages. These languages are compiled to WebAssembly (WASM) bytecode where they can be executed off-chain in the WAVS runtime.
The examples in this documentation are mainly written in Rust, but there are also examples of components written in the following languages:
Component structure
A basic component has three main parts:
- Decoding incoming trigger data.
- Processing the data (this is the custom business logic of your component).
- Encoding and returning the result for submission (if applicable).
Trigger inputs
When building WASI components, keep in mind that components can receive the trigger data in two ways:
-
On-chain events: When triggered by an EVM event, the data comes through the
TriggerAction
withTriggerData::EvmContractEvent
. -
Local testing: When using
make wasi-exec
command in the template to test a component, the data comes throughTriggerData::Raw
. No abi decoding is required, and the output is returned as raw bytes.
Here's how the example component handles both cases in trigger.rs
:
// In trigger.rspub fn decode_trigger_event(trigger_data: TriggerData) -> Result<(u64, Vec<u8>, Destination)> {match trigger_data {// On-chain Event// - Receive a log that needs to be decoded using the contract's ABI// - Decode into our Solidity types generated by the sol! macro from the Solidity interfaceTriggerData::EvmContractEvent(TriggerDataEvmContractEvent { log, .. }) => {// Decode Ethereum event logs using the `decode_event_log_data!` macrolet 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))}// Local Testing (wasi-exec)// - Receive raw bytes directly// - No ABI decoding is neededTriggerData::Raw(data) => Ok((0, data.clone(), Destination::CliOutput)),_ => Err(anyhow::anyhow!("Unsupported trigger data type")),}}mod solidity { // Define the Solidity types for the incoming trigger event using the `sol!` macrouse alloy_sol_macro::sol;pub use ITypes::*;// The objects here will be generated automatically into Rust types.// the interface shown here is used in the example trigger contract in the template.sol!("../../src/interfaces/ITypes.sol");}
The component decodes the incoming event trigger data using the decode_event_log_data!
macro from the wavs-wasi-utils
crate.
The sol!
macro from alloy-sol-macro
is usedto define Solidity types in Rust. This macro reads a Solidity interface file and generates corresponding Rust types and encoding/decoding functions. For more information, visit the Blockchain interactions page.
Component logic
Components must implement the Guest
trait, which is the main interface between your component and the WAVS runtime.
The run
function is the entry point for your business logic: it receives and decodes the trigger data, processes it according to your component's logic, and returns the results.
// In lib.rsimpl Guest for Component {fn run(action: TriggerAction) -> Result<Option<WasmResponse>, String> {// 1. Decode the trigger data using the decode_trigger_event function from trigger.rslet (trigger_id, req, dest) = decode_trigger_event(action.data)?;// 2. Process the data (your business logic)let res = block_on(async move {let resp_data = get_price_feed(id).await?;serde_json::to_vec(&resp_data)})?;// 3. Encode the output based on destinationlet output = match dest {// For on-chain submissions, the output is abi encoded using the encode_trigger_output function from trigger.rsDestination::Ethereum => Some(encode_trigger_output(trigger_id, &res)),// For local testing via wasi-exec, the output is returned as raw bytesDestination::CliOutput => Some(WasmResponse {payload: res.into(),ordering: None}),};Ok(output)}}
Components can contain any compatible logic, including blockchain interactions, network requests , off-chain computations, and more. To learn about the types of components that WAVS is best suited for, visit the design considerations page.
Logging in a component
Components can use logging to debug and track the execution of the component.
Logging in development:
Use println!()
to write to stdout/stderr. This is visible when running make wasi-exec
locally in the template.
println!("Debug message: {:?}", data);
Logging in production
For production, you can use a host::log()
function which takes a LogLevel
and writes its output via the tracing mechanism. Along with the string that the developer provides, it attaches additional context such as the ServiceID
, WorkflowID
, and component Digest
.
use bindings::host::{self, LogLevel};host::log(LogLevel::Info, "Production logging message");
Component output
After processing data in the run
function, the component can encode the output data for submission back to Ethereum. In the template example, this is done using the encode_trigger_output
function in the trigger.rs file.
/// 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(), // ABI encode the struct for blockchain submissionordering: None, // Optional ordering parameter for transaction sequencing}}
Outputs for components are returned as a WasmResponse
struct, which is a wrapper around the output data of the component for encoding and submission back to Ethereum. It contains a payload
field that is the encoded output data and an optional ordering
field that is used to order the transactions in the workflow. The WasmResponse
is submitted to WAVS which routes it according to the workflow's submission logic.
Component definition
A component is defined in the workflow object of the service.json file. Below is an example of the different fields that can be defined in the component object.
// ... other parts of the service manifest// "workflows": {// "workflow-example": {// "trigger": { ... }, the trigger for the workflow"component": { // the WASI component containing the business logic of the workflow"source": { // Where the component code comes from"Registry": {"registry": {"digest": "882b992af8f78e0aaceaf9609c7ba2ce80a22c521789c94ae1960c43a98295f5", // SHA-256 hash of the component's bytecode"domain": "localhost:8090","version": "0.1.0","package": "example:evmrustoracle"}}},"permissions": { // What the component can access"allowed_http_hosts": "all", // Can make HTTP requests to any host"file_system": true // Can access the filesystem},"fuel_limit": null, // Computational limits for the component"time_limit_seconds": 1800, // Can run for up to 30 minutes"config": { // Configuration variables passed to the component"variable_1": "0xb5d4D4a87Cb07f33b5FAd6736D8F1EE7D255d9E9", // NFT contract address"variable_2": "0x34045B4b0cdfADf87B840bCF544161168c8ab85A" // Reward token address},"env_keys": [ // Secret environment variables the component can access from .env"WAVS_ENV_API_KEY", // secret API key with prefix WAVS_ENV_]},// "submit": { ... } // submission logic for the workflow// ... the rest of the service manifest
For more information on component configuration variables and keys, visit the variables page.
Registry
WAVS uses a registry to store the WASM components. A service like wa.dev is recommended for proper distribution in production. A similar registry environment is emulated locally in docker compose for rapid development without an API key:
- Build your component
- Compile the component
- Upload the component to the registry
- Set the registry in your service using the wavs-cli command in the
build_service.sh
script:
wavs-cli workflow component --id ${WORKFLOW_ID} set-source-registry --domain ${REGISTRY} --package ${PKG_NAMESPACE}:${PKG_NAME} --version ${PKG_VERSION}