# WAVS Documentation This file contains all WAVS documentation in a format optimized for large language models. Website: https://docs.wavs.xyz --- # WAVS Docs URL: / Description: Welcome page with links to WAVS documentation sections ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/wavs.png) Welcome to the WAVS Docs! [WAVS](https://github.com/Lay3rLabs/WAVS) (Web Assembly Verifiable Services) is the event-driven framework for distributed architectures, allowing you to ship verifiable applications faster with our all-in-one framework. Use this documentation to learn [about WAVS](/overview), [how it works](/how-it-works), and how to [start building your verifiable service](./tutorial/1-overview). Head over to the [WAVS Github](https://github.com/Lay3rLabs/WAVS) and give it a star ⭐! ## Get started } href="/overview" title="WAVS Overview" description="Learn about WAVS" /> } href="/how-it-works" title="How it works" description="Explore the inner workings of WAVS" /> } href="/benefits" title="WAVS Benefits" description="Discover how WAVS revolutionizes verifiable app development" /> } href="/tutorial/1-overview" title="Build a service" description="Follow the WAVS tutorial to build a service" /> --- # WAVS benefits URL: /benefits Description: Key advantages and use cases of WAVS platform ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/banners/benefits.png) >WAVS is a platform that helps you ship verifiable apps faster. **The problem**: Developers building decentralized applications currently face an impossible choice: - Run everything on-chain: Prohibitively expensive and slow - Rely on centralized servers: Defeats the purpose of decentralization - Build custom infrastructure: Months of scaffolding and work across separate bridges, oracles, and serverless functions Developing a verifiable app the traditional way is complicated. It requires a lot of preliminary development work, such as building custom contracts, scaffolding infrastructure, working with Dockerized components, and coordinating with operators. Most of the development centers around creating the app’s infrastructure, which is generally more complicated than the core logic of the service itself. **There is an easier way**: WAVS (Web Assembly Verifiable Services) provides an alternative: verifiable off-chain compute through a unified event-driven framework. WAVS simplifies bridges, oracles and other third party server functions into on-chain, offchain, and internal events. When an event is triggered, operators execute the application workflow as components in the WAVS engine and submit results to be verified on-chain. All of this happens natively within one all-in-one framework, making decentralized verifiable application development fast and simple. WAVS provides the layer of infrastructure so you can focus solely on creating the core logic of your service. Write logic in languages you already know (Rust, Golang, JavaScript/TypeScript, C/C+ or Python) and compiled as a lightweight WASI component which can be deployed to the WAVS Engine and run as a service by operators. These components are run offchain by operators in the WAVS runtime at near-native speed, and the results are brought verifiably on-chain. A service of services, WAVS allows verifiable apps to dynamically run and manage multiple components that work together to build flexible and intelligent applications. ## Why WAVS? WAVS redefines the verifiable application infrastructure paradigm, making them easier to build, less expensive to run, and enabling the next generation of composable, intelligent blockchain services. **Speed**: Developers skip the boilerplate and focus on building their application so teams can ship fast. Quickly build application components with templates and built-in tooling for parsing events, aggregating signatures, and handling coordination logic. Developers can add, update, or modify components on the fly without coordinating changes across an entire operator set. Developers can build in languages they already know like Rust, Golang, JavaScript/TypeScript, or Python instead of being slowed down by new smart contract languages **Cost savings**: With WAVS, you don’t pay for three separate systems—oracles, bridge integrations, and 3rd party serverless functions are handled as first-class events in one framework. Your core logic runs in standard languages, so you can hire regular software engineers and you can choose the chain, security model, and deployment shape that makes sense for your budget and roadmap. **Peace of mind**: Build on WAVS confidently without worrying that you’re betting the company on the wrong stack. WAVS is designed to be reliable, portable, and adaptable, so your application logic isn’t trapped in one chain, one execution model, or one vendor’s roadmap. You can change chains, security models, and verification methods over time as your product grows without rewriting core logic. WAVS is open source with no lock-in, so you can build confidently without surprises. --- # WAVS design considerations URL: /design Description: Best practices and design patterns for WAVS services The WAVS approach to application design focuses on the lightweight and agile nature of components, with an emphasis on performance and security. WAVS works best with the "serverless function" approach, wherein the priority of service component logic is to process data from external sources rather than store it locally. In this model, a component can receive input data from external sources, process it, and submit the verifiable outputs to external sources. ## Aggregation and deterministic queries WAVS currently supports "exact match" [aggregation](./handbook/submission#aggregator), meaning that consensus is reached if a threshold amount of submitted responses from operators are identical. This approach fits many common use cases, but it means that developers must build their components to receive and process data only from deterministic or immutable sources, such as: - Data from the input event trigger - Ethereum queries at a given block height - IPFS data or other Content-Addressable Storage - Web2 APIs that are trusted to return the same result on every query - Seeded application parameters (e.g. Ollama for AI models) For example, when designing a price oracle, a **blockheight** or **timestamp** should be specified in the query logic. This ensures that the query is deterministic and that the same data point is retrieved by all operators. Conversely, a component that is designed to fetch "current price" would be non-deterministic and may result in a consensus error. Operators may run components at slightly different times, leading to discrepancies in the data they receive. This design should be avoided, as aggregation for this would require custom BFT averaging logic which is not currently supported. However, adding custom logic to process non-exact matches will be available in future releases. ## State Persistent state introduces additional challenges in application design with WAVS: operators may execute triggers at different times or, in some cases, not run them at all if they join an protocol after it has been running for some time. Components that rely on operator-local mutable state risk failing consensus due to inconsistencies in execution. For these reasons, it is best practice to avoid storing operator-local mutable state within your components. This functionality would require features such as state synchronization (P2P), guaranteed execution ordering, and Merkle-proof validation which are not yet supported. ## Caching WAVS provides components with an optional local data directory that can be used for caching. This storage method should only be used to cache data as a performance optimization and not as a replacement for external state stores. It is best practice when using a cache to reference external data from deterministic immutable sources to avoid consensus failures. Keep the following points in mind when using cached data: 1. Caching should be used as a performance optimization only. 2. Use immutable or deterministic external data sources with specific time stamps or block heights. 3. Design your component logic to work even if the cache is cleared. Cache state is not shared among operators, and any operator should be able to rebuild the cache from scratch. 4. Don't use caching to store state that depends on previous executions or mutable data. --- # Overview URL: /overview Description: Introduction to WAVS platform and its core concepts ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/banners/intro.png) ## What is WAVS? Developers building decentralized applications face an impossible choice: - **Run everything on-chain** — Prohibitively expensive and slow - **Rely on centralized servers** — Defeats the purpose of decentralization - **Build custom infrastructure** — Months of scaffolding across separate bridges, oracles, and serverless functions [WAVS](https://github.com/Lay3rLabs/WAVS) (Web Assembly Verifiable Services) is the event-driven framework for distributed architectures that provides a third option: verifiable off-chain compute through a unified framework. Developers define events (on-chain, off-chain, or internal), trigger workflows that execute components via the WAVS Engine, and submit verifiable outputs on-chain. Bridges, oracles, and other third-party server functions become first-class events — handled natively within one framework instead of stitched together across separate systems. Write logic in languages you already know — Rust, Golang, JavaScript/TypeScript, or Python — compiled to [WASM](./how-it-works#wasm-and-wasi) and executed at near-native speed. WAVS separates services from the operators that run them: builders define components, operators execute them off-chain, sign the results, and commit them on-chain. Off-chain compute with on-chain verifiability. ## Why WAVS? **Ship faster** — Skip the boilerplate and focus on your application. Built-in tooling handles event parsing, signature aggregation, and coordination logic. Components can be added, updated, or modified without coordinating changes across an operator set. Build in languages your team already knows — no new smart contract languages required. **Lower costs** — One framework replaces three separate systems. Oracles, bridge integrations, and serverless functions are all first-class events in WAVS, so you're not paying for and maintaining each independently. Your core logic runs in standard languages, so you can hire general-purpose engineers and choose the chain, security model, and deployment shape that fits your budget and roadmap. **No lock-in** — Your application logic isn't trapped in one chain, one execution model, or one vendor's roadmap. WAVS is compatible with ZK proofs, TEEs, MPC, and multiple security models (PoA, restaking). Switch chains, security models, or verification methods as your product grows — without rewriting core logic. WAVS is open source with no business license. ## Use cases WAVS supports a wide range of use cases, enabling powerful, verifiable off-chain computation across different domains: - **Decentralized AI**: WAVS unlocks decentralized AI that is [deterministic and verifiable](https://www.layer.xyz/news-and-insights/deterministic-ai), enabling trustless decision-making and autonomous governance through DAO-controlled AI agents. - **Oracles**: [Create](./tutorial/1-overview) and dynamically deploy new oracles as easily as uploading a component to your service to verifiably bring data from any source on-chain. - **Zero Knowledge Proofs**: ZK verifiers, ZK Prover Marketplaces, and ZK aggregation layers can be deployed as lightweight WAVS [service components](#service-components), making it simple to build modular and composable proof services. - **Crosschain Bridges**: Build secure, decentralized bridges with WAVS. Lock assets on one chain, trigger a verifiable service component, and mint them on another—all with trust-minimized execution. - **Dynamic applications**: WAVS combines on-chain and off-chain services to build a cross-chain, decentralized application layer. - **Intelligent protocols**: Build protocols that are verifiably aware of on-chain and off-chain events without relying on centralized infrastructure. Compose services and applications to enable complex webs of decentralized transaction flows. - **TEEs**: WAVS can be used to build TEEs (Trusted Execution Environments) that run off-chain computations in a secure and verifiable manner, ensuring data integrity and confidentiality. ## Building a service WAVS removes the complexity of building a verifiable application, making it easy to develop and deploy custom services. With built-in infrastructure and developer tooling, WAVS powers a new multichain ecosystem of composable, decentralized, and verifiable applications. **Learn more:** The following is a basic overview of a WAVS service. For a more in-depth overview of WAVS, visit the [How it works section](./how-it-works). Check out the WAVS tutorial to learn how to build a service. This example will cover a basic verifiable service with three parts: a trigger, a service component, and submission logic. ### Defining triggers Triggers are the actions or events that prompt your service to be run. Currently, WAVS supports triggers from on-chain events from EVM and Cosmos chains, cron schedules, and block intervals for EVM or Cosmos chains. Triggers can be used to pass arbitrary data as the inputs for service components to be run. Operators running a service listen for specific trigger events and run the corresponding service component. ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/diagrams/trigger.png) To learn more about triggers, visit the [triggers page](./handbook/triggers). ### Service components Service components are the core logic of a verifiable service. They are written in (Rust, Golang, JavaScript/TypeScript, C/C+ or Python)(./handbook/components/component#languages) and compiled to [WASM](./how-it-works#wasm-and-wasi) as lightweight WASI components. WAVS provides a base layer of infrastructure, allowing you to focus solely on the logic of your service. Service components can contain logic for processing input data from triggers. If a trigger passes data, a service component can use that data as input. For example, a simple service component could contain logic for receiving a number as input and returning the square of that number as output. ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/diagrams/service.png) To learn more about service components, visit the [How it works](/how-it-works#service-components) page. Check out the [WAVS tutorial](./tutorial/1-overview) to learn how to create a service component. ### Submission logic Along with your component, you'll also need to define how the results of your service are submitted on-chain. With WAVS, you can use an [aggregator and submission contract](/how-it-works#submission) to define this logic. ### Run your service Builders define their service in a [service manifest or service.json file](./handbook/service) with a workflow that includes a trigger, service component, and submission logic. Registered operators will then listen for the triggers specified by your service. Once triggered, operators will run your service off-chain, where data from a trigger is passed to a service component and run in a sandboxed WASI environment. Operators sign the result of the service computation and the verified result can be returned as an on-chain response. ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/diagrams/wavs-flow.png) With WAVS, service updates are streamlined: updates to services can be made by builders directly without needing to coordinate with operators. Operators only need to upgrade if there is a change to the WAVS node software itself. ## Multichain capability WAVS is built to be multichain. A service can be triggered by events on one chain, run by operators off-chain, and write verified responses to another chain. This interoperability is what makes WAVS so flexible, creating a decentralized computational layer that can function across multiple chains. ## Composability WAVS is composable by nature, allowing a verifiable service to dynamically run and manage multiple workflows that work together to build flexible and intelligent applications. Workflows include a trigger, service component, and submission logic. To [compose services and workflows](./handbook/workflows), the trigger of one workflow can be the submission logic of another workflow. The registry model allows component bytecode to be stored in one location and reused across multiple workflows. ## Security WAVS has a pluggable security layer — you choose the model that fits your application, from a simple permissioned deployment to full cryptoeconomic staking. - **Proof of Authority (PoA)** — The simplest option. A contract owner (typically a deployer account, DAO, or multisig) manages the operator set directly, assigning each operator a numeric weight. No staking or external protocol required. The [PoA middleware](https://github.com/Lay3rLabs/poa-middleware) is audited and production-ready, making it a solid default for most services. - **TEEs (Trusted Execution Environments)** — Components can run inside hardware-based TEEs such as Intel TDX or AWS Nitro Enclaves. TEEs provide confidential execution and hardware attestation, useful for services that require data privacy or need to prove that a specific binary ran unmodified. - **Restaking** — Provides cryptoeconomic security by leveraging staked assets from a restaking protocol. Operators stake and are subject to slashing for misbehavior, aligning economic incentives with correct execution. [EigenLayer](https://www.eigenlayer.xyz/) and [Symbiotic](https://symbiotic.fi/) are examples of restaking protocols supported by WAVS. ## Full-stack decentralization WAVS enables full-stack decentralization by unifying off-chain computation with on-chain verifiability. The chain layer connects to Ethereum and other chains, while the security layer provides flexible, verifiable operator accountability — via PoA, TEEs, restaking, or a combination. At the app layer, lightweight WAVS powers WASM-based services to process off-chain computations triggered by on-chain events. Operators validate, sign, and commit the results back on-chain, ensuring verifiability and trust-minimized execution. This architecture makes WAVS a scalable, decentralized framework for full-stack applications. --- # How it works URL: /how-it-works Description: Technical overview of WAVS architecture and components ![WAVS](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/banners/how.png) WAVS is the event-driven framework for distributed architectures that helps teams ship decentralized, verifiable applications faster. Developers define events (on-chain, off-chain, or internal), trigger workflows that execute components via the WAVS Engine, and submit verifiable outputs on-chain. ## The flow Before diving into each part of a WAVS service individually, it's important to understand the basic execution flow of WAVS. ![WAVS overview](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/diagrams/workflows.png) 1. A service is a collection of on-chain contracts and off-chain logic run by a set of registered operators, bringing the results of off-chain computation on-chain verifiably. A service's operator set is defined in a service manager contract. A service manifest (service.json) defines the configuration and different parts of a WAVS service, including information about the service, workflows, components, submission, watch trigger, aggregator, and more. The service contracts and manifest are deployed and operators register to run the service. 2. Services contain one or more workflows, which define the different execution paths in your service. Each workflow contains a trigger, a service component, and submission logic. Triggers are defined events that activate a workflow. Registered operators running WAVS nodes maintain lookup maps to track and detect triggers. Triggers can be: - Any on-chain EVM or Cosmos event from a specified contract address and event signature - Cron jobs that activate at specified intervals - Blockheight triggers that activate at a specified block height on EVM or Cosmos chains - AT Protocol (Bluesky) events for a specified collection and action - Hypercore feed appends for a specified feed key 3. When a trigger is detected, operators run the workflow's corresponding service component off-chain in the WAVS runtime. 4. Each operator signs the result of their off-chain computation and submits it to the aggregator. 5. The aggregator accepts results from the operators and verifies that the result payloads match and that the signatures are valid. Once enough verified submissions are received (the threshold is defined in the service manager), the aggregator submits the bundled signatures and result payloads as a single transaction to the submission contract specified in the service's manifest. 6. The `handleSignedEnvelope()` function on the submission contract validates the data and signatures via the service manager contract. The workflow is complete, bringing the result of the off-chain computation verifiably on-chain. Services and workflows can be chained together by setting the submission event of one workflow to be the trigger of another. ## WAVS parts ### Service manager The service manager is a contract that defines a service's operator set. It is used to register operators and define their weights, set the threshold for aggregator submissions, and maintain the URI of the service manifest. For more information, see the [Service page](./handbook/service#service-manager). ### Service manifest The service manifest is a JSON file that defines the configuration and different parts of a WAVS service, including information about the service, workflows, components, submission, service manager contract, aggregator configuration, and more. For more information, see the [Service page](./handbook/service). ### Workflows Workflows are the building blocks of a WAVS service flow. They define the different execution paths of a service, including the trigger, component, and submission logic. For more information, see the [Workflows page](./handbook/workflows). ### Triggers A trigger is the event that activates a WAVS service. Triggers can be EVM or Cosmos events, cron, or blockheight triggers. When a specified event is triggered, WAVS operators detect it and execute the corresponding service component off-chain. The results are then verified and submitted back on-chain, completing the execution cycle. For more information, see the [Triggers page](./handbook/triggers). ### Service components Service components are the heart of the WAVS platform, encapsulating the core logic that powers a service. They contain the off-chain business logic for a service, [written in Rust, Golang, JavaScript/TypeScript, C/C+ or Python](./handbook/components/component#component-languages). These service components are compiled to WASM and are uploaded to the WAVS platform where they can be run by operators. In the WAVS runtime, service components are sandboxed from each other and from the node's operating system. This way, operators and verifiable services maintain a clean separation, with builders defining service components and operators having to opt in to each service. Service components are lightweight and built for adaptability. Building a service used to take thousands of lines of code and the configuration of dozens of files to execute even the simplest logic. With WAVS, service components can consist of a few lines of code that can be dynamically deployed to the WAVS platform. Service components are also designed for composability: an app can chain multiple components together, creating decentralized flows of off-chain and on-chain executions. These intelligent protocols merge the performance of off-chain computation with on-chain verifiability, creating a complex mesh of decentralized transaction flows. To learn more, visit the [Components page](./handbook/components/component). For a hands-on example of a service component, visit the [tutorial](./tutorial/1-overview). ### WAVS runtime The WAVS runtime serves as the off-chain execution environment for all services running on the WAVS platform. Powered by operators running the WAVS node software, it provides a structured framework for deploying and managing service components that run within a WASI (Web Assembly System Interface) environment. You can think of WASI as a lightweight OS-like interface, offering a standard set of APIs that allow service components to perform system-level operations such as file handling and environment interactions. WAVS enables service components to execute securely within this WASI-powered sandbox, ensuring isolation from both the host system and other components. ### WASM and WASI [WASM (Web Assembly)](https://webassembly.org/) is a high-performance, low-level binary format that can be compiled from multiple programming languages. WASM can even run in web browsers at near-native speed. By leveraging WASM, applications built with WAVS are lightning-fast, lightweight, and easy to develop. WASI (WebAssembly System Interface) is a standardized API that enables WASM (WebAssembly) modules to interact with a host system in a secure and platform-independent way. It provides WASM modules with a standardized set of APIs to access system resources. For more information, visit the [WASI documentation](https://wasi.dev/). There are significant advantages in leveraging a WASM/WASI-based platform for AVSs: - Lightweight execution: Service components are lightweight WASM binaries ideal for high-frequency, low-latency tasks. - Speed: Components can run in the WASI environment at near-native speeds, providing a significant advantage over Dockerized apps. - Low overhead: Instead of each service needing its own dedicated Docker container, the WAVS runtime provides a computational layer that can be used by many components, saving storage and startup time. - Dynamic deployment: To upgrade a service, simply upload a new component and update your service metadata to point to the new component. No more downtime or coordination of new binaries among operators. - Security and separation: The WAVS WASI runtime enforces security by sandboxing service components, allowing them to interact with the host (WAVS) only through explicitly permitted WASI APIs. ### Registry WAVS uses a registry to store the WASM components. A service like [wa.dev](https://wa.dev) is recommended for proper distribution in production. For more information, visit the [Component page](./handbook/components/component#registry). ### Signing and aggregation When a service is triggered, each operator registered to the service will run the service component on their machine and generate the result. These results are signed by the operator before being submitted to an aggregator. For services that submit results on Ethereum, operators sign their results and submit them off-chain to an aggregator rather than directly on-chain. The aggregator collects responses, verifies each operator's signature, and compares the payloads. A submission is only made once enough verified responses arrive to satisfy the quorum defined in the service manager — an insufficient quorum is rejected by design. When the threshold is met, the aggregator bundles the signatures and result into a single on-chain transaction, reducing gas overhead compared to individual operator submissions. Services that submit on-chain require a custom [aggregator component](./handbook/components/aggregator) that determines when and where to submit. The aggregator component runs inside the WAVS aggregator process and implements three callbacks: `process_input` (called when enough responses arrive), `handle_timer_callback` (for deferred submission), and `handle_submit_callback` (called after on-chain submission). To learn more about the aggregator, visit the [Submission and aggregator page](./handbook/submission#aggregator). For more discussion on aggregation considerations, visit the [Design considerations page](./design). ### Submission A submission contract is the final destination of the results of a service component. Known as a Service Handler, this logic can be any contract as long as it implements the [`handleSignedEnvelope()` function using the `IWavsServiceHandler`](./handbook/submission#submission-contract) interface to validate data and signatures using the service manager. After the execution of a service component in the WAVS runtime, the results are aggregated in the [aggregator](./handbook/submission#aggregator) and passed to the submission contract. Developers can use this contract to define their own logic for results and implement different rules depending on the specific needs of their service. To learn more about the submission contract, visit the [Submission page](./handbook/submission). ### Updating a service Because of the lightweight and portable nature of WebAssembly, WAVS operators only need to run a single Docker image. WAVS provides a runtime for all registered services to run, each sandboxed from the other and from the node's operating system due to the nature of [WASI](#wasm-and-wasi). Operators will need to opt in to running different services by registering with each service's middleware contracts, and the service must be deployed to each node. Updates to service logic do not require node upgrades. Instead, developers can dynamically deploy a new service component and update their service manifest. Instead of needing to run a new Docker image every time a service is updated, operators only need to upgrade if there is a breaking change to the WAVS node software itself. --- # 1. Oracle service tutorial URL: /tutorial/1-overview Description: Introduction to WAVS tutorial series ![Start-building](https://raw.githubusercontent.com/Lay3rLabs/WAVS-docs/refs/heads/main/public/start-building.png) In this guide, you will build a simple oracle service that fetches Bitcoin price data from [coinmarketcap.com](https://coinmarketcap.com/api/). This example is built using the [WAVS Foundry Template](https://github.com/Lay3rLabs/wavs-foundry-template), which contains the tools you need to build your own custom service. The price oracle service example has three basic parts: 1. [A trigger contract](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src/contracts/WavsTrigger.sol): A trigger can be any on-chain event emitted from a contract. This event **triggers** a service to run. In the WAVS Foundry Template, there is a simple trigger contract that stores trigger requests, assigns them unique IDs, and emits an event when a new trigger is added. In this example, the trigger event `addTrigger` will pass data pertaining to the ID of an asset for the CoinMarketCap price feed. 2. [A service component](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/evm-price-oracle/src/lib.rs): The service component contains the business logic of a service. It is written in Rust, compiled to WASM, and run by operators in the WAVS runtime. In this example, operators will listen for a new trigger event to be emitted and then run the service component off-chain, using the asset ID data from the trigger event as input. The component contains logic to fetch the price of the asset from the CoinMarketCap price feed API, which is then processed and encoded before being sent back on-chain. 3. [A submission contract](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src/contracts/WavsSubmit.sol): Also known as the "service handler," this contract contains the on-chain submission logic for the service. It validates and stores the processed data returned by the WAVS component. When an operator submits a response, the contract verifies the data's integrity by checking the operator's signature and then associates it with the original trigger ID, bringing the queried price on-chain. These three parts come together to create a basic oracle service using WAVS. To learn more about services and how they work, visit the [How it works page](../how-it-works). ## Video tutorial You can follow along with this guide by watching the video tutorial: } href="/tutorial/2-setup" title="Get Started" description="Click here to set up your environment and start building your service." /> --- # 2. System setup URL: /tutorial/2-setup Description: Setting up development environment for WAVS The following installations are required to run this example. Follow the steps below to set up your system. **System recommendations:** This tutorial is designed for Windows WSL, Linux, and macOS systems. **Linux essentials:** If you are on Linux (e.g. Ubuntu), install the build essentials first: ```bash docci-ignore sudo apt update && sudo apt install build-essential ``` ## Rust Run the following command to install [Rust](https://www.rust-lang.org/tools/install). ```bash docci-ignore curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` **Homebrew Rust not supported:** If you installed Rust using Homebrew, you will need to uninstall it and install it again using the rustup command. ```bash docci-ignore brew uninstall rust ``` Then run: ```bash docci-ignore curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` **Fresh Install:** If you just installed Rust for the first time, you will need to run the following commands: ```bash docci-ignore # Install required target and toolchain rustup toolchain install stable rustup target add wasm32-wasip2 ``` **Upgrade Rust:** If you already have a previous version of Rust installed, you will need to run the following commands to upgrade it to the latest stable version: ```bash docci-ignore # Remove old targets if present rustup target remove wasm32-wasi || true rustup target remove wasm32-wasip1 || true # Update and add required target rustup update stable rustup target add wasm32-wasip2 ``` ## Cargo components Install the following for building WebAssembly components. Visit the [Cargo Component documentation](https://github.com/bytecodealliance/cargo-component#installation) for more information. {/* This section is also in [](./5-build.mdx). Remember to update there as well */} ```bash docci-ignore cargo install cargo-binstall cargo binstall cargo-component wasm-tools warg-cli wkg --locked --no-confirm --force # Configure default registry # Found at: $HOME/.config/wasm-pkg/config.toml wkg config --default-registry wa.dev # Allow publishing to a registry # # if WSL: `warg config --keyring-backend linux-keyutils` warg key new ``` **WSL Ubuntu GLIB out of date:** If you are on Ubuntu LTS but encounter an error like `wkg: /lib/x86_64-linux-gnu/libm.so.6: version 'GLIBC_2.38' not found (required by wkg)`: ``` sudo do-release-upgrade ``` ## Foundry [Foundry](https://book.getfoundry.sh/) is a solidity development suite. The Foundry toolchain contains Anvil (a local testnet node), Forge (build and test smart contracts), Cast (an RPC call CLI), and Chisel (a Solidity REPL). 1. Install Foundryup, the official Foundry installer. ```bash docci-ignore curl -L https://foundry.paradigm.xyz | bash ``` 2. Install Foundry ```bash docci-ignore foundryup ``` ## Docker Visit the [Docker Documentation](https://docs.docker.com/get-started/get-docker/) for more info. ```bash docci-ignore brew install --cask docker ``` ```bash docci-ignore sudo apt -y install docker.io ``` Install [Docker Desktop with WSL](https://docs.docker.com/desktop/wsl/#turn-on-docker-desktop-wsl-2), then run: ```bash docci-ignore sudo chmod 666 /var/run/docker.sock ``` **Avoid sudo for Docker:** `sudo` is only required for Docker commands if your user is not in the Docker group. To avoid using `sudo` with Docker, add your user to the Docker group: ```bash docci-ignore sudo groupadd docker && sudo usermod -aG docker $USER ``` After adding yourself to the group, log out and back in for the changes to take effect. **Docker for MacOS:** {/* This section is also in [](./5-build.mdx). Remember to update there as well */} If prompted, remove container with `sudo apt remove containerd.io` If you are using Docker Desktop, make sure it is open and running for this tutorial. Before proceeding, make sure that the following setting is updated: **Enable Host Networking**: Open Docker and navigate to -> Settings -> Resources -> Network. Make sure that 'Enable Host Networking' is turned on. Alternatively, you can install the following: ```bash docci-ignore brew install chipmk/tap/docker-mac-net-connect && sudo brew services start chipmk/tap/docker-mac-net-connect ``` If you are running on a Mac with an ARM chip, you will need to do the following: - Set up Rosetta: ```bash docci-ignore softwareupdate --install-rosetta ``` - Enable Rosetta (Docker Desktop: Settings -> General -> enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon") ## Task Visit the [Task Documentation](https://taskfile.dev/) for more info. ```bash docci-ignore brew install go-task ``` ```bash docci-ignore npm install -g @go-task/cli ``` ## Docker Compose Visit the [Compose Documentation](https://docs.docker.com/compose/) for more info. Docker Compose is already included with Docker Desktop. ```bash docci-ignore sudo apt-get install docker-compose-v2 ``` ## pnpm Install the latest version of [pnpm](https://pnpm.io/installation). pnpm is used by `task setup` to install JavaScript dependencies. ## JQ Visit the [JQ Documentation](https://jqlang.org/download/) for more info. ```bash docci-ignore brew install jq ``` ```bash docci-ignore sudo apt -y install jq ``` ## Node.js Node v21+ is needed for the WAVS template. Visit the [NVM Installation guide](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating) to install Node Version Manager and update your Node version. ``` curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash nvm install --lts ``` After setting up your system, continue to the next page to create your project. --- # 3. Create your project URL: /tutorial/3-project Description: Creating and configuring WAVS project structure {/* todo: update --branch main once the template is updated */} 1. After setting up your environment, open a terminal and run the following command to create your WAVS Foundry Template project. In this example, your project will be called `my-wavs`. ```bash docci-ignore forge init --template Lay3rLabs/wavs-foundry-template my-wavs --branch main ``` 2. Then, enter your project: ```bash docci-ignore cd my-wavs ``` 3. Run the following command to open your project in VS code, or open it in the editor of your choice: ```bash docci-ignore code . ``` ## Explore the template This template repo contains all the files you'll need to build, run, and test WAVS services locally. The template already contains the necessary files for the oracle example to run. For example, the trigger ([`WavsTrigger.sol`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src/contracts/WavsTrigger.sol)) and submission ([`WavsSubmit.sol`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src/contracts/WavsSubmit.sol)) contracts can be found in the [`/my-wavs/src/contracts`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src) folder. In [`/evm-price-oracle/src/lib.rs`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/evm-price-oracle/src/lib.rs) you'll find the oracle service component. This template uses a [`Taskfile.yml`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/Taskfile.yml) and environment variables to help with your developer experience. If you are ever curious about one of the `task` commands in the following sections, you can always look at the [`Taskfile.yml`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/Taskfile.yml) to learn more. **Info:** You can run the following command from the root of the repo to see all of the commands and environment variable overrides available: ```bash docci-ignore task help ``` The next sections will show you how to deploy your contracts and components, set up WAVS, and run the oracle example. ## Build and test your contracts Run the following commands from the root of your project to install necessary dependencies, build the template contracts, and run tests using Forge. ``` # Install dependencies task -y setup # Build the contracts forge build # Run the solidity tests. forge test ``` The last command runs a basic unit test which verifies that the `SimpleTrigger` contract in `/WavsTrigger.sol` correctly stores and retrieves trigger data. --- # 4. Oracle component walkthrough URL: /tutorial/4-component Description: evm-price-oracle component walkthrough The core logic of the price oracle in this example is located in the [`/evm-price-oracle/src/lib.rs` file](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/evm-price-oracle/src/lib.rs). Scroll down to follow a walkthrough of the code for the oracle component. ## !!steps trigger.rs The [trigger.rs](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/evm-price-oracle/src/trigger.rs) file handles the decoding of incoming trigger data from the trigger event emitted by the trigger contract. The component uses the `decode_event_log_data!()` macro from the [wavs-wasi-utils crate](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/) to decode the event log data and prepares it for processing within the WAVS component. The `Destination` enum 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](../handbook/triggers). To learn more about trigger input handling, visit the [Component page](../handbook/components/component#trigger-inputs). ```rust ! 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, Destination)> { match trigger_data { TriggerData::EvmContractEvent(TriggerDataEvmContractEvent { log, .. }) => { let event: solidity::NewTrigger = decode_event_log_data!(log)?; let trigger_info = ::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) -> 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](../handbook/components/blockchain-interactions) for more information on the `sol!` macro and how to use it to generate Rust types from Solidity interfaces. ## !!steps Oracle component logic The [`lib.rs`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/evm-price-oracle/src/lib.rs) file contains the main component logic for the oracle. The first section of the code 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() }) } ``` ## !!steps 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. ```rust ! 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](../handbook/submission). 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. ```rust ! trigger.rs pub fn encode_trigger_output(trigger_id: u64, output: impl AsRef) -> 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](../handbook/components/component#component-output). To learn more about submission logic, visit the [Submission page](../handbook/submission). The Service handbook contains more detailed information on each part of developing services and components. Visit the [Service handbook overview](../handbook/overview) to learn more. ## Next steps Continue to the [next section](./5-build) to learn how to build and test your component. --- # 5. Build and test components URL: /tutorial/5-build Description: Building and testing WAVS service components **Before proceeding:** {/* This section is also in [](./2-setup.mdx). Remember to update there as well */} 1. Make sure that Docker is installed. If you are using Docker Desktop, make sure it is open and running. If you are using Mac OS, make sure that your[ Docker app is configured correctly](./2-setup#docker). 2. Make sure that you have already run the following commands from the [system setup section](./2-setup#cargo-components). ```bash docci-ignore cargo install cargo-binstall cargo binstall cargo-component wasm-tools warg-cli wkg --locked --no-confirm --force # Configure default registry wkg config --default-registry wa.dev # Allow publishing to a registry # # if WSL: `warg config --keyring-backend linux-keyutils` warg key new ``` ## Build components Run the following command in your terminal to build your component. Exclude `WASI_BUILD_DIR` to build all components. ```bash docci-if-file-not-exists="./compiled/evm_price_oracle.wasm" WASI_BUILD_DIR=components/evm-price-oracle task -y build:wasi ``` This command will build any components present in the `/components` directory, as well as auto-generate bindings and compile the components to WASM. The output will be placed in the `compiled` directory. **Build command:** You can also use the command below to build your solidity contracts and components in one command: ```bash docci-if-file-not-exists="./compiled/evm_price_oracle.wasm" task build:all ``` ## Testing and debugging You can use the following command to execute the component using Cast. This command is handy for testing components without having to deploy WAVS. An ID of 1 is Bitcoin. Nothing will be saved on-chain, just the output of the component is shown. ``` INPUT_DATA="1" COMPONENT_FILENAME=evm_price_oracle.wasm task wasi:exec ``` This command runs your component locally in a simulated environment and lets you easily view `print` statements for debugging. Running this command in the oracle example will print the information from the oracle [component code](./4-component). Visit the [component walkthrough](../handbook/components/component#logging-in-a-component) for more information on logging during testing and production. Upon successful execution, you should receive a result similar to the following: ```bash docci-ignore resp_data: Ok(PriceFeedData { symbol: "BTC", timestamp: "2025-02-14T01:23:03.963Z", price: 96761.74120116462 }) INFO Fuel used: 1477653 Result (hex encoded): 7b2273796d626f6c223a22425443222c2274696d657374616d70223a22323032352d30322d31345430313a32333a30332e3936335a222c227072696365223a39363736312e37343132303131363436327d Result (utf8): {"symbol":"BTC","timestamp":"2025-02-14T01:23:03.963Z","price":96761.74120116462} ``` **Fuel:** In the output above, the `INFO Fuel used` value represents the computational power consumed during execution. Similar to how on-chain transactions have a gas limit to cap transaction costs, WAVS enforces a fuel limit to control off-chain computational workload and protect against DoS attacks. The maximum fuel allocation can be adjusted in the `Taskfile` to accommodate different processing needs. --- # 6. Run your service URL: /tutorial/6-run-service Description: Deploying and running WAVS services **Foundry template:** This tutorial uses the [wavs-foundry-template](https://github.com/Lay3rLabs/wavs-foundry-template) and its Taskfile-based workflow. Real-world WAVS projects may use different tooling and project structures. ## Local: Start Anvil, WAVS, and deploy service manager 1. Create a `.env` file for your project by copying over the example with the following command: ``` cp .env.example .env ``` 2. Use the following command to start an Anvil test chain, IPFS, Registry, and some optional telemetry. This only runs with `LOCAL` being set in the `.env` (default). ```bash docci-background docci-delay-after=5 task -y start-all-local ``` **Keep WAVS running:** The command must remain running in your terminal. Open another terminal to run other commands. You can stop the services with `ctrl+c`. Some MacOS terminals require pressing this twice. With the chain running, you can deploy and run your service. ## Deploy Run the following single command to automate the complete WAVS deployment process: ```bash docci-delay-after=30 task deploy-full ``` This handles everything end-to-end: - Creating a deployer account and funding it - Deploying the PoA service manager contract - Deploying the trigger and submission Solidity contracts - Uploading the compiled WASI component - Building and uploading the service manifest to IPFS - Starting the aggregator and registering the service with it - Starting WAVS and deploying the service - Registering operators with the service manager When complete, a deployment summary is written to `.docker/deployment_summary.json` with the deployed contract addresses. ## Trigger the service Next, use your deployed trigger contract to trigger the oracle to be run. In the following command, you'll specify the `INPUT_DATA` as abi encoded `1`, which corresponds to the ID of Bitcoin. Running this command will execute [`/src/script/Trigger.s.sol`](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/src/script/Trigger.s.sol) and pass the ID to the trigger contract, starting the following chain of events: 1. The trigger contract will emit an event with the specified ID as its data. 2. Operators listening for the event will receive the data and run it in the oracle component off-chain. 3. The oracle component will use the ID to query the price of Bitcoin from the CoinMarketCap API. 4. The returned data will be signed by operators and passed to the [aggregator and then the submission contract](../handbook/submission), which will verify the operator's signature and submit the price of Bitcoin on-chain 🎉 ```bash docci-delay-per-cmd=2 # Get the trigger address from the deployment summary export SERVICE_TRIGGER_ADDR=`jq -r '.evmpriceoracle_trigger.deployedTo' .docker/deployment_summary.json` export RPC_URL=`task get-rpc` export FUNDED_KEY=`task config:funded-key` # Request BTC price from CoinMarketCap (ID=1) export INPUT_DATA=`cast abi-encode "addTrigger(string)" "1"` forge script ./src/script/Trigger.s.sol ${SERVICE_TRIGGER_ADDR} ${INPUT_DATA} --sig 'run(string,string)' --rpc-url ${RPC_URL} --broadcast --private-key ${FUNDED_KEY} ``` ## Show the result Run the following to view the result of your service in your terminal: ```bash docci-delay-per-cmd=2 docci-output-contains="BTC" export SERVICE_SUBMIT_ADDR=`jq -r '.evmpriceoracle_submit.deployedTo' .docker/deployment_summary.json` RPC_URL=${RPC_URL} forge script ./src/script/ShowResult.s.sol ${SERVICE_SUBMIT_ADDR} 1 --sig 'data(string,uint64)' --rpc-url ${RPC_URL} ``` Congratulations, you've just made a simple Bitcoin price oracle service using WAVS! Proceed to the [Prediction Market demo](./7-prediction) to learn how a similar oracle service can be used in a prediction market. Check out the [Service handbook](../handbook/overview) to learn more about services and creating components. --- # AI-powered component creation URL: /handbook/ai Description: Use Claude or Cursor to create one-shot components with minimal prompting The WAVS Foundry Template contains built-in AI rulefiles for creating "one-shot" components with minimal prompting in Cursor or Claude Code. These rulefiles are an experimental feature and may not work as expected every time. Components created with AI should not be used in production without thorough review and testing. **LLM resources:** For more information on AI tools and AI-accessible documentation, visit the [LLM resources page](/resources/llms). ## Claude Code - Follow the [Claude Code installation instructions](https://docs.anthropic.com/en/docs/claude-code/getting-started) to install Claude Code and link your account. - The Claude rulefile is `claude.md` and contains instructions for Claude on how to create a component. - Learn more about Claude rulefiles: https://docs.anthropic.com/en/docs/claude-code/memory ## Cursor - Download Cursor: https://www.cursor.com/downloads - The Cursor rulefiles are located in the `.cursor/rules` directory. - When using Cursor, always attach the `component-rules.mdc` file to the chat with your prompt. - Learn more about Cursor rulefiles: https://docs.cursor.com/context/rules ## Using AI to create components 1. Clone the [WAVS Foundry Template](https://github.com/Lay3rLabs/wavs-foundry-template) and follow the system setup requirements in the README. ``` git clone https://github.com/Lay3rLabs/wavs-foundry-template.git cd wavs-foundry-template git checkout main # Follow the system setup requirements in the README. ``` 2. Open Claude Code or Cursor in the root of the template. ``` claude # or cursor . ``` **Sandboxed Claude Code:** You can run a sandboxed instance of [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) in a Docker container that only has access to this project's files by running the following command from the root of the project: ```bash docci-ignore npm run claude-code # or with no restrictions (--dangerously-skip-permissions) npm run claude-code:unrestricted ``` 3. Enter your prompt in the agent chat. You can use the following examples as a starting point, or you can create your own prompt. **Attaching rulefiles:** If you are using cursor, always attach `component-rules.mdc` file to the chat with your prompt. ``` @component-rules.mdc ``` ### Prompt examples These simple examples are provided to get you started. #### API component You can make a very simple prompt to create a component that can bring API responses verifiably onchain by including the API endpoint: ``` Let's make a component that takes the input of a zip code, queries the openbrewerydb, and returns the breweries in the area. @https://api.openbrewerydb.org/v1/breweries?by_postal=92101&per_page=3 ``` #### Contract balance component You can also make components that interact with the blockchain: ``` I want to build a new component that takes the input of a wallet address, queries the usdt contract, and returns the balance of that address. ``` #### Verifiable AI component ``` Please make a component that takes a prompt as input, sends an api request to OpenAI, and returns the response. Use this api structure: { "seed": $SEED, "model": "gpt-4o", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": ""} ] } My api key is WAVS_ENV_OPENAI_KEY in my .env file. ``` You'll need an [OPENAI API account and key](https://platform.openai.com/login) to use this prompt. The agent will include your API key in the component as a [private variable](./components/variables). Make sure to include your API key in a `.env` file: ``` # copy the .env.example file cp .env.example .env # place your key in .env (must be prefixed with WAVS_ENV_) WAVS_ENV_OPENAI_KEY=your_api_key ``` This example utilizes the OpenAI API with a [seed](https://platform.openai.com/docs/advanced-usage#reproducible-outputs) to make the response more deterministic. Please note that OpenAI models are not guaranteed to be 100% deterministic. This example is for demonstration purposes and should not be used in production. ## Component creation process 4. After receiving the prompt, the agent will start creating your component. Review the agent's work and accept changes carefully. Make sure to double check what the agent is doing and be safe about accepting changes. 5. The agent will start by planning its component and will create a `plan.md` file. The agent will then make a new component and files according to this plan. 6. The agent will test its component for errors by running validation tests using `task validate-component COMPONENT=your-component`. 7. The agent may need to make changes after running the Validation tests. After making changes, the agent will build the component using `WASI_BUILD_DIR=components/my-component task -y build:wasi`. 8. After successfully building your component, it's time to test it. The following command can be used to test your component logic without deploying WAVS. Make sure to replace the placeholders with the correct inputs. ``` # Run this command to build the component: WASI_BUILD_DIR=components/openai-response task -y build:wasi # Once built, test it with: INPUT_DATA="Only respond with yes or no: Is AI beneficial to the world?" COMPONENT_FILENAME=openai_response.wasm task wasi:exec ``` The agent may try to run the `task wasi:exec` command themselves. You should prompt the agent to give you the command instead, as it can't run the command without permissions. 9. Your component should execute and return a response. If there are any errors, share them with the agent for troubleshooting. If you have any questions, join the WAVS DEVS Telegram channel: https://t.me/layer_xyz/818 ## Tips for working with AI agents - While this repo contains rulefiles with enough context for creating simple components, coding agents are unpredictable and may inevitably run into problems. - Feel free to update the rulefiles for your specific purposes or if you run into regular errors. - Coding agents can sometimes try to over-engineer their fixes for errors. If you feel it is not being productive, it may be beneficial to start fresh. You may need to adjust your prompt. - If you are building a complex component, it may be helpful to have the agent build a simple component first and then expand upon it. - The agent may try to fix warnings unnecessarily. You can tell the agent to ignore minor warnings and any errors found in `bindings.rs` (it is auto-generated). ### Prompting This repo is designed to be used with short prompts for simple components. However, often, coding agents will do better with more context. When creating a prompt, consider the following: - Agents work best with short, clear instructions. - Provide relevant documentation (preferably as an `.md` file or other ai-digestible content). - Provide endpoints. - You may need to provide API response structure if the agent is not understanding responses. - Be specific about what you want the agent to build. - Agents work systematically to build components. For best results, agent should make a plan before they start building. - Be patient. Coding agents are not perfect. They may make mistakes. ## Troubleshooting - You can ask the agent to fix errors it may not be able to catch when executing components. Make sure to give the agent full context of the error. - LLMs can be unpredictable. Minimal prompts provide a lot of room for creativity/error. If the agent is not able to fix an error after trying, sometimes deleting the component, clearing the history, and starting fresh can help. - The agent may try to edit the bindings.rs file to "fix" it. The agent never needs to do this, and you should tell the agent to not do this. - The agent is supposed to provide you with the `task wasi:exec` command. Sometimes it will try to run this itself and it will fail. Instead, ask it to give you the command. - When copying and pasting the full `task wasi:exec` command, be careful with line breaks. You may need to reformat long lines to avoid breaking the command. --- # Task commands URL: /handbook/commands Description: CLI commands for WAVS development The `Taskfile.yml` in the template composes commands from two sources: - **Project-specific taskfiles** in `./taskfile/` — build, deploy, and service tasks customized for the template - **Shared base taskfiles** from [Lay3rLabs/wavs-taskfiles](https://github.com/Lay3rLabs/wavs-taskfiles) — common operator, docker, WASI, and core tasks reused across WAVS projects ## Commands Use `task help` (or `task --list-all`) to see all the commands: ``` task help ``` Here are the available `task` commands and their descriptions: ``` help show available tasks setup install initial dependencies start-all-local start all local services (anvil, IPFS, WARG, Jaeger, prometheus) deploy-full run complete WAVS deployment pipeline build:all build everything (Solidity + WASI components) build:forge build Solidity contracts build:wasi build WASI components (all or specific one) | WASI_BUILD_DIR wasi:exec execute the WAVS wasi component(s) | COMPONENT_FILENAME, INPUT_DATA deploy:service deploy WAVS service from URL | SERVICE_URL, WAVS_ENDPOINT deploy:ipfs upload service config to IPFS | SERVICE_FILE deploy:component upload WASI component to WAVS endpoint | COMPONENT_FILENAME, WAVS_ENDPOINT operator:register register an operator with WAVS Service Manager operator:update-signing-key update operator's signing key with WAVS Service Manager operator:verify verify operator registration status get-trigger-from-deploy get the trigger address from the script deploy get-submit-from-deploy get the submit address from the script deploy show-result show the result | SERVICE_SUBMISSION_ADDR, TRIGGER_ID, RPC_URL test run tests lint:check check linting and formatting lint:fix fix linting and formatting issues ``` For more information on commands when using the template, visit the [WAVS tutorial](/tutorial/1-overview). --- # Handbook overview URL: /handbook/overview Description: Guide to WAVS handbook structure and contents **Follow the tutorial:** {/* todo: verify all links are correct. */} Before reading this guide, follow the [Oracle component tutorial](/tutorial/1-overview) to learn the basics of building a WAVS service. This handbook provides an overview of the different parts that make up a WAVS application. ## Tutorial - [Oracle component tutorial](/tutorial/1-overview) - Start here to learn the basics of building a WAVS service. ## Core Concepts - [How it works](../how-it-works) - Learn about the different parts that make up a WAVS service. - [Design](../design) - Learn about the design considerations for building a WAVS service. ## Services - [Service](./service) - Learn about WAVS services, their structure, and how they are defined in the service manifest. - [Workflows](./workflows) - Understand how workflows define execution paths in your service, including triggers, components, and submissions. - [Triggers](./triggers) - Explore the different types of triggers that can initiate your service, including EVM events, Cosmos events, cron jobs, and block intervals. - [Submission and Aggregator](./submission) - Discover how results are submitted to the blockchain through the aggregator service and submission contracts. ## Components - [Component overview](./components/component) - Learn about the structure and lifecycle of WAVS components, including how to handle triggers and process data. - [Variables](./components/variables) - Understand how to configure components with public and private variables. - [Blockchain interactions](./components/blockchain-interactions) - Discover how to interact with blockchains and smart contracts from your components. - [Network requests](./components/network-requests) - Learn how to make HTTP requests to external APIs from your components. ## Development - [Template](./template) - Get started with the WAVS template, including its structure, configuration files, and how to customize it for your service. - [Makefile commands](./commands) - Reference for the available makefile commands to build, deploy, and manage your service. - [AI-powered component creation](./ai) - Learn how to use AI coding agents to create components. Each section provides detailed information and examples to help you understand and build your WAVS service. Start with the Service section to understand the basic concepts, then explore the other sections based on your needs. --- # Service manifest and manager URL: /handbook/service Description: Overview of the service.json manifest file and service manager contract A service is a collection of smart contracts, operators, and offchain components that make up a WAVS application. The different parts of a service are defined in a service manifest or `service.json` file. This file can be stored on IPFS or an HTTP/HTTPS server, and its URL is set on the service manager contract during deployment, allowing the system to fetch the service definition when needed. The service manifest defines the configuration and different parts of a WAVS service, including information about the service, [workflows](/handbook/workflows), [components](/handbook/components/component), [submission](/handbook/submission), [service manager contract](#service-manager), and more. ## Generate Manifest You can create the service.json file using the `wavs-cli service` command. The template provides a script to generate a single-component service with ease, [build_service.sh](https://github.com/Lay3rLabs/wavs-foundry-template/blob/main/script/build-service.sh). ## Example Manifest ```json service.json { // Basic service information "id": "example-service-123", "name": "Example WAVS Service", // Workflows define the different execution paths in your service "workflows": { // Each workflow has a unique ID "default": { // Trigger defines what initiates the workflow "trigger": { // This example uses an EVM contract event trigger "evm_contract_event": { "chain_name": "ethereum", "address": "0x1234567890123456789012345678901234567890", "event_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" } }, // Component defines the WASI component to execute "component": { "source": { "Registry": { "registry": { // (SHA-256) hash that identifies a specific version of a WASI component's code in WAVS "digest": "882b992af8f78e0aaceaf9609c7ba2ce80a22c521789c94ae1960c43a98295f5", // The domain the warg WASI component is hosted on "domain": "localhost:8090", // The version of this component "version": "0.1.0", // The package name of the component on the domain registry "package": "example:evmrustoracle" } } }, // Permissions define what the component can access "permissions": { // HTTP host permissions "allowed_http_hosts": "all" // Options: "all", ["host1", "host2"], or "none" // File system permissions "file_system": true }, // Resource limits "fuel_limit": 1000000, // Optional: Maximum compute units "time_limit_seconds": 30, // Optional: Maximum execution time // Component-specific configuration "config": { "endpoint": "https://api.example.com", "timeout": "30s" }, // Environment variables to be passed to the component "env_keys": [ "WAVS_ENV_API_KEY", "WAVS_ENV_SECRET" ] }, // Submit defines where the results are sent "submit": { // The aggregator configuration "aggregator": { "url": "http://127.0.0.1:8001" } }, "aggregators": [ { "evm": { // The identifier for the chain the submission contract (set in wavs.toml) "chain_name": "ethereum", // The address of the submission contract with the service handler interface "address": "0xfedcba9876543210fedcba9876543210fedcba98", // The maximum amount of gas for submission "max_gas": 1000000 // Optional } } ] } //other workflows can be added here }, // Service status "status": "active", // Options: "active" or "inactive" // Service manager configuration "manager": { "evm": { "chain_name": "ethereum", // The address of the service manager contract "address": "0xabcdef1234567890abcdef1234567890abcdef12" } } } ``` ## Upload This file should be uploaded to IPFS, or some other hosted service that all operators can access. The template launches a local IPFS for testing. Use a service like [Pinata](https://app.pinata.cloud/developers/api-keys) for production services. ``` # Upload to local or remote IPFS (smart routes based on .env deploy configuration) SERVICE_FILE=${SERVICE_FILE} make upload-to-ipfs # smart grabs the IPFS gateway and fetches the content that was uploaded export IPFS_GATEWAY=$(sh script/get-ipfs-gateway.sh) curl "${IPFS_GATEWAY}${ipfs_cid}" # Then the admin of the contracts can set it cast send ${WAVS_SERVICE_MANAGER_ADDRESS} 'setServiceURI(string)' "${SERVICE_URI}" -r ${RPC_URL} --private-key ${DEPLOYER_PK} ``` For more information on the different parts of a service manifest, see the following sections: - [Workflows](./workflows) - [Triggers](./triggers) - [Components](./components/component) - [Submission and aggregator](./submission) ## Service manager The service manager contract defines the set of registered operators for a service. Only operators registered in this contract are considered valid signers for result submissions in a service. Each registered operator is assigned a weight. These weights count toward a threshold for their submission power. The service manager also maintains a service URI that points to the service manifest, connecting the operators to the service. Signatures are created by operators using their private keys to sign an envelope containing the data, and these signatures are collected by the aggregator which then submits them to the service manager contract for validation. The service manager contract validates that the signatures are from registered operators, checks that their total weight meets the threshold, and ensures the operators are properly sorted before allowing the data to be processed by the [service handler](/handbook/submission) contract. --- # Submission and aggregator URL: /handbook/submission Description: Creating and configuring submission contracts and the aggregator This page describes the submission and aggregator services used to submit results from a workflow to a submission contract on an EVM chain. ## Submit definition The `submit` field in a service.json file specifies the submission logic for a service. The `aggregator` type sends results to an aggregator service, which validates the results and submits them to a target contract on an EVM chain. ```json service.json "submit": { // Where results are sent "aggregator": { // Type of submission (aggregator) "url": "http://127.0.0.1:8001" // Local aggregator endpoint } }, "aggregators": [ // The final submission address that the aggregator will submit to { "evm": { // EVM chain configuration "chain_name": "local", // Local Ethereum chain "address": "0xd6f8ff0036d8b2088107902102f9415330868109", // Submission contract address "max_gas": 5000000 // Maximum gas limit for transactions } } ] ``` Submit can also be set to `none` if the service does not need to submit results to a contract. The component will still run, but the results will not be submitted. ## Submission contract A service handler or submission contract handles the logic for verifying the submission of a component's output to the blockchain. The only requirement for a submission contract is that it must implement the `handleSignedEnvelope()` function using the `IWavsServiceHandler` interface to validate data and signatures using the service manager. This interface is defined in the `@wavs` package: https://www.npmjs.com/package/@wavs/solidity?activeTab=code **Chaining workflows:** Workflows can be chained together by setting the trigger event of one workflow to the submission event of another workflow. For more information on chaining workflows, see the [Workflows page](./workflows). ## Template submission example The [template submission contract](https://github.com/Lay3rLabs/wavs-foundry-template/blob/main/src/contracts/WavsSubmit.sol) uses the `handleSignedEnvelope()` function to validate operator signatures and store the processed data from the component. The `DataWithId` struct must match the output format from the component. In the template, each trigger has a unique ID that links the data to its source. Below is a simplified version of the template submission contract: ```solidity WavsSubmit.sol // Contract must implement IWavsServiceHandler to receive data // ITypes provides the DataWithId struct and other type definitions contract SimpleSubmit is ITypes, IWavsServiceHandler { /// @notice Service manager instance - used to validate incoming data and signatures IWavsServiceManager private _serviceManager; /** * @notice Initialize the contract with a service manager * @param serviceManager The service manager instance that will validate data */ constructor(IWavsServiceManager serviceManager) { _serviceManager = serviceManager; } /// @inheritdoc IWavsServiceHandler /// @notice Main entry point for receiving and processing data /// @param envelope Contains the event ID and the actual data payload /// @param signatureData Contains operator signatures for validation function handleSignedEnvelope(Envelope calldata envelope, SignatureData calldata signatureData) external { // First validate the data and signatures through the service manager // This ensures the data is properly signed by authorized operators _serviceManager.validate(envelope, signatureData); // Decode the payload into your expected data structure // The payload format must match what your component outputs DataWithId memory dataWithId = abi.decode(envelope.payload, (DataWithId)); // At this point, you can safely process the validated data // Add your custom logic here to handle the data } } ``` ## Aggregator **Aggregator component required:** As of WAVS v1.0.0, services that submit on-chain must include a custom aggregator component. The aggregator component controls submission timing and target addresses. See the [Aggregator component guide](./components/aggregator) for how to build one. The aggregator is used to collect and validate responses from multiple operators before submitting them to the blockchain. It acts as an intermediary that receives signed responses from operators, validates each operator's signature, aggregates signatures when enough operators have responded, and submits the aggregated data to the submission contract. The aggregator supports exact match aggregation, meaning that consensus is reached if a threshold amount of submitted responses from operators are identical. Visit the [design considerations page](/design) for more information on aggregation and service design. WAVS currently uses ECDSA signatures for aggregation. ## Aggregator submission flow 1. An operator runs a component which returns a `WasmResponse` containing: - `payload`: The result data - `ordering`: Optional ordering information 2. The operator creates an Envelope containing the result data and signs it with their private key, creating an signature. 3. A Packet containing the envelope, signature, and route information (service ID and workflow ID) is created and sent to the aggregator's `/packet` endpoint. 4. The aggregator validates the packet's signature by recovering the operator's address and adds it to a queue of packets with the same trigger event and service ID. 5. When enough packets accumulate to meet the threshold (determined by the service manager contract), the aggregator: - Combines the signatures from all packets into a single SignatureData structure - Validates the combined signatures on-chain through the service manager contract 6. If validation succeeds, the aggregator sends the operator signatures and payload result data as a single transaction to the `handleSignedEnvelope()` function on the submit contract specified in the service's manifest. 7. The `handleSignedEnvelope()` function validates the data and signatures via the service manager contract. --- # Template overview URL: /handbook/template Description: Overview of the WAVS foundry template **Follow the tutorial:** Before reading this guide, follow the [Oracle component tutorial](/tutorial/1-overview) to learn the basics of building a WAVS service. Use the info in this guide to customize the template to create your own custom service. Check out the [WAVS design considerations](/design) page to learn which use-cases WAVS is best suited for. ## Foundry Template structure [The WAVS template](https://github.com/Lay3rLabs/wavs-foundry-template) is made up of the following main files: ``` wavs-foundry-template/ ├── README.md ├── Taskfile.yml # Commands, variables, and configs ├── components/ # WASI components │ └── evm-price-oracle/ │ ├── src/ │ │ ├── lib.rs # Main Component logic │ │ ├── trigger.rs # Trigger handling │ │ └── bindings.rs # Bindings generated by `task build:all` │ └── Cargo.toml # Component dependencies ├── compiled/ # WASM files compiled by `task build:all` ├── src/ │ ├── contracts/ # Trigger and submission contracts │ └── interfaces/ # Solidity interfaces ├── script/ # Deployment & interaction scripts ├── wavs.toml # WAVS service configuration ├── docs/ # Documentation ├── .cursor/rules/ # Cursor AI rulefiles ├── claude.md # Claude AI rulefile └── .env # Private environment variables ``` - The `README` file contains the tutorial commands. - The `Taskfile.yml` contains commands for building and deploying the service. It also contains configurable variables for the service and deployment. - The `components` directory contains the component logic for your service. Running `task build:wasi` will automatically generate bindings and compile components into the `compiled` directory. - The `src` directory contains the Solidity contracts and interfaces for trigger and submission contracts. - The `script` directory contains the scripts used in the Taskfile commands to deploy, trigger, and test the service. - The `.env` file contains private environment variables and keys. Use `cp .env.example .env` to copy the example `.env` file. - The `.cursor/rules` directory and `claude.md` file contain rulefiles for [building components with Cursor AI and Claude AI agents](/handbook/ai). ## Toml files There are several toml files in the template that are used to configure the service: - `wavs.toml` is used to configure the WAVS service itself, including chains (local, testnets, mainnet) and configurations. - `Cargo.toml` in the root directory is used to configure the workspace and includes dependencies, build settings, and component metadata. - `components/*/Cargo.toml` in each component directory is used to configure the Rust component and includes dependencies, build settings, and component metadata. It can inherit dependencies from the root `Cargo.toml` file using `workspace = true`. These files can be customized to suit your specific needs, and many settings can be overridden using environment variables. The following is an example of a component's `Cargo.toml` file structure: ``` [package] name = "evm-price-oracle" edition.workspace = true version.workspace = true authors.workspace = true rust-version.workspace = true repository.workspace = true [dependencies] wit-bindgen-rt ={ workspace = true } wavs-wasi-utils = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } alloy-sol-macro = { workspace = true } wstd = { workspace = true } alloy-sol-types = { workspace = true } anyhow = { workspace = true } [lib] crate-type = ["cdylib"] [profile.release] codegen-units = 1 opt-level = "s" debug = false strip = true lto = true [package.metadata.component] package = "component:evm-price-oracle" target = "wavs:operator@=1.2.0" ``` ## wavs.toml config The [`wavs.toml`](https://github.com/Lay3rLabs/wavs-foundry-template/blob/main/wavs.toml) file contains configuration settings for all WAVS components: - Default general settings (shared across all processes) - WAVS server-specific settings - CLI-specific settings - Aggregator-specific settings ### Environment Variable Overrides Environment variables can override configuration values using these patterns: - WAVS server settings: `WAVS_` - CLI settings: `WAVS_CLI_` - Aggregator settings: `WAVS_AGGREGATOR_` --- # Triggers URL: /handbook/triggers Description: Setting up and managing WAVS service triggers {/* todo: verify all code examples. link to appropriate pages. */} A trigger prompts a WAVS service to run. Operators listen for the trigger event specified by the service and execute the corresponding component off-chain. Triggers can be any onchain event emitted from any contract. ### Trigger lifecycle 1. A service is deployed with a service.json manifest which contains information on the service and [workflow](/handbook/workflows) logic (components, triggers, [submission](/handbook/submission) logic). 2. Operators maintain lookup maps to track and verify triggers. For EVM and Cosmos events, they map chain names, contract addresses, and event identifiers to trigger IDs. Block interval triggers are tracked by chain name with countdown timers, while cron triggers are managed in a priority queue ordered by execution time. 3. When a trigger is detected, operators verify it against their lookup maps according to trigger type. If a match is found, a TriggerAction is created with the trigger configuration and event data. 4. `TriggerAction` has 2 fields: `TriggerConfig` which contains the service, workflow, and trigger configuration, and `TriggerData`contains the trigger data based on the trigger type. ``` pub struct TriggerAction { pub config: TriggerConfig, // Contains service_id, workflow_id, and trigger type pub data: TriggerData, // Contains the actual trigger data } pub struct TriggerConfig { pub service_id: ServiceID, pub workflow_id: WorkflowID, pub trigger: Trigger, } pub enum TriggerData { CosmosContractEvent { //For Cosmos event triggers contract_address: layer_climb_address::Address, /// The address of the contract that emitted the event chain_name: ChainName, /// The name of the chain where the event was emitted event: cosmwasm_std::Event, /// The data that was emitted by the contract block_height: u64, /// The block height where the event was emitted }, EvmContractEvent { //For EVM event triggers contract_address: alloy_primitives::Address, /// The address of the contract that emitted the event chain_name: ChainName, /// The name of the chain where the event was emitted log: LogData, /// The raw event log block_height: u64, /// The block height where the event was emitted }, BlockInterval { //For block interval triggers chain_name: ChainName, /// The name of the chain where the blocks are checked block_height: u64, /// The block height where the event was emitted }, Cron { //For cron triggers trigger_time: Timestamp, /// The trigger time }, AtprotoEvent { // For Bluesky/AT Protocol event triggers sequence: i64, /// Monotonically increasing sequence number timestamp: i64, /// Event timestamp repo: String, /// DID of the repository (e.g. "did:plc:...") collection: String, /// Lexicon collection (e.g. "app.bsky.feed.post") rkey: String, /// Record key within the collection action: String, /// Operation type: "create", "update", or "delete" cid: Option, /// Content identifier of the record (None for deletes) record_data: Option, /// JSON-encoded record contents (None for deletes) }, HypercoreAppend { // For Hypercore feed append triggers feed_key: String, /// Public key identifying the Hypercore feed index: u64, /// Index of the newly appended block in the feed data: Vec, /// Raw bytes of the appended block }, Raw(Vec), // Raw bytes — used for local testing with `wasi-exec` } ``` 5. The TriggerAction is converted to a WASI-compatible format and passed to the component where it is decoded and processed. The component decodes the incoming event trigger data using the `decode_event_log_data!` macro from the [`wavs-wasi-utils` crate](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/macro.decode_event_log_data.html). Visit the [components page](./components/component) for more information on decoding and processing trigger data in your component. ## Trigger configuration Triggers define when and how the component should be executed. Each workflow needs a trigger to be set. They are set in the `trigger` field of the [`service.json` file](/handbook/service). ### EVM event trigger This trigger listens for specific events emitted by contracts on EVM-compatible chains, executing the component when a matching event is detected. Event triggers pass raw log data to the component. ``` "trigger": { "evm_contract_event": { "address": "0x00000000219ab540356cbb839cbe05303d7705fa", // Contract address to monitor "chain_name": "ethereum", // Chain to monitor "event_hash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" // Event hash (32 bytes) } } ``` Your evm chain information must be set in `wavs.toml` under the `[default.chains.evm.]` section: ```toml wavs.toml # Mainnet [default.chains.evm.ethereum] chain_id = "1" ws_endpoints = ["wss://eth.drpc.org"] # Supports multiple endpoints for automatic failover http_endpoint = "https://eth.drpc.org" ``` You'll need to set your EVM chain credential in your `.env` file to establish a connection for monitoring the EVM chain: ```env .env WAVS_CLI_EVM_CREDENTIAL="0x5ze146f435835b1762ed602088740d201b68fd94bf808f97fd04588f1a63c9ab" ``` ### Cosmos event trigger This trigger monitors events emitted by Cosmos smart contracts, executing your component when a matching event type is detected from the specified contract address. Cosmos event triggers pass the contract data that was emitted by the contract to the component. ``` "trigger": { "cosmos_contract_event": { "address": { "Cosmos": { "bech32_addr": "neutron1qlaq54uh9f52d3p66q77s6kh9k9ee3vasy8gkdkk3yvgezcs6zts0mkcv4", // Contract address to monitor "prefix_len": 7 // Length of the Bech32 prefix (7 for Neutron) } }, "chain_name": "neutron", // Chain to monitor "event_type": "send_nft" // Event type to watch } }, ``` Your chain information must be set in `wavs.toml` under the `[default.chains.cosmos.]` section: ```toml wavs.toml # == Cosmos chains == [default.chains.cosmos.neutron] chain_id = "pion-1" bech32_prefix = "neutron" rpc_endpoint = "https://rpc-falcron.pion-1.ntrn.tech" grpc_endpoint = "http://grpc-falcron.pion-1.ntrn.tech:80" gas_price = 0.0053 gas_denom = "untrn" ``` Your Cosmos mnemonic must be set in your `.env` file to establish a connection for monitoring the Cosmos chain: ```env .env WAVS_CLI_COSMOS_MNEMONIC="large slab plate twenty laundry illegal vacuum phone drum example topic reason" ``` ### Cron trigger Executes your component on a schedule defined by a cron expression, with optional start and end times to control the execution window. If no start or end time is provided, the component will start immediately and run indefinitely. Cron triggers pass the trigger timestamp to the component. ``` "trigger": { "cron": { "schedule": "0 */5 * * * *", // Every 5 minutes (at 0 seconds) "start_time": 1704067200000000000, // Optional start time in nanoseconds since Unix epoch (2024-01-01T00:00:00Z) (default: null) "end_time": 1735689599000000000 // Optional end time in nanoseconds since Unix epoch (default: null) } } // Cron Expression Format: // * * * * * * // │ │ │ │ │ │ // │ │ │ │ │ └── Day of week (0-6, where 0 is Sunday) // │ │ │ │ └──── Month (1-12) // │ │ │ └────── Day of month (1-31) // │ │ └──────── Hour (0-23) // │ └────────── Minute (0-59) // └──────────── Second (0-59) // // Each field can be: // - A number: `5` (exact time) // - A range: `1-5` (1 through 5) // - A list: `1,3,5` (1, 3, and 5) // - A step: `*/5` (every 5 units) // - An asterisk: `*` (every unit) // // Common examples: // - `0 */5 * * * *` - Every 5 minutes (at 0 seconds) // - `0 0 */6 * * *` - Every 6 hours (at 0 minutes and 0 seconds) // - `0 0 0 * * *` - Every day at midnight (00:00:00) // - `0 0 12 * * *` - Every day at noon (12:00:00) // - `0 0 12 1 * *` - Noon on the first day of every month // - `0 0 12 * * 1` - Noon every Monday ``` [Crontab.guru](https://crontab.guru/) is a helpful tool for making cron expressions. **Cron trigger latency:** There may be slight variations in Cron trigger execution time between operators due to network latency and clock drift. Cron triggers are best suited for tasks that don't require precise synchronization between operators: - Triggering components that don't need exact synchronization. - Collecting data from external services with monotonic pagination. - Background tasks where eventual consistency is acceptable. If you need precise timing synchronization between operators, consider using [block-based triggers](#block-trigger) instead. ### Block trigger Executes your component at regular block intervals on a specified EVM or Cosmos chain, useful for chain-specific operations that need to run periodically. Block interval triggers pass the block height and chain name to the component. ``` "trigger": { "block_interval": { "chain_name": "ethereum-mainnet", "n_blocks": 10, "start_block": null, // Optional blockheight to start "end_block": null // Optional blockheight to end } } ``` ### AT Protocol (Bluesky) event trigger Executes your component when a record is created, updated, or deleted on the [AT Protocol](https://atproto.com/) network (the protocol underlying Bluesky). Useful for building services that react to social activity, content moderation, or decentralized social graph changes. ``` "trigger": { "atproto_event": { "collection": "app.bsky.feed.post", // Lexicon collection to watch "action": "create" // "create", "update", or "delete" } } ``` The `TriggerData::AtprotoEvent` variant passed to your component contains: ``` TriggerData::AtprotoEvent(TriggerDataAtprotoEvent { sequence, // Monotonically increasing sequence number timestamp, // Event timestamp repo, // DID of the repository (e.g. "did:plc:...") collection, // Lexicon collection (e.g. "app.bsky.feed.post") rkey, // Record key within the collection action, // "create", "update", or "delete" cid, // Content identifier (None for deletes) record_data, // JSON-encoded record contents (None for deletes) .. }) ``` ### Hypercore append trigger Executes your component when a new block is appended to a [Hypercore](https://hypercore-protocol.org/) feed. Useful for building services that process peer-to-peer data streams. ``` "trigger": { "hypercore_append": { "feed_key": "abc123..." // Public key of the Hypercore feed to watch } } ``` The `TriggerData::HypercoreAppend` variant contains: ``` TriggerData::HypercoreAppend(TriggerDataHypercoreAppend { feed_key, // Public key identifying the Hypercore feed index, // Index of the newly appended block data, // Raw bytes of the appended block }) ``` ### Raw trigger (local testing) The `Raw` variant is not a service trigger type configured in `service.json`. It is the trigger data format used when executing a component locally with `task wasi:exec` in the template. Raw bytes are passed directly to the component without any ABI decoding. ``` // In trigger.rs — handle the Raw variant for local testing TriggerData::Raw(data) => Ok((0, data.clone(), Destination::CliOutput)), ``` See the [component overview](./components/component#trigger-inputs) for a full example of handling both on-chain and raw trigger data in the same component. --- # Workflows URL: /handbook/workflows Description: Building and managing WAVS service workflows A WAVS service is a collection of one or more workflows that define the different execution paths in your service. Each workflow consists of three parts: - [**Trigger**](./triggers): Defines what event initiates the workflow - [**Component**](./components/component): The WASM component that processes the event - [**Submit**](./submission): Specifies where to send the results ## Workflow Structure Workflows are defined in the service manifest JSON file, which contains the necessary information on which trigger, component, and submission logic are needed. The following example shows a workflow with a cron trigger and a submission to an aggregator: ```json service.json // ... other parts of the service manifest "workflows": { //workflows are added here "0196c34d-003d-7412-a3f3-70f8ec664e12": { // a unique workflow ID (default is a generated UUID v7) "trigger": { // Defines what starts the workflow "cron": { // Type of trigger (cron job) "schedule": "0 * * * * *", // Runs every minute at 0 seconds "start_time": null, "end_time": null } }, "component": { // the WASI component containing the business logic of the workflow "source": { // Where the component code comes from "Digest": "65747b4b1a7fa98cab6abd9a81a6102068de77b1040b94de904112272b226f51" // SHA-256 hash of the component's bytecode }, "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 passed to the component "nft": "0xb5d4D4a87Cb07f33b5FAd6736D8F1EE7D255d9E9", // NFT contract address "reward_token": "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": { // Where results are sent "aggregator": { // Type of submission (aggregator) "url": "http://127.0.0.1:8001" // Local aggregator endpoint } }, "aggregators": [ // The final submission address that the aggregator will submit to { "evm": { // EVM chain configuration "chain_name": "local", // Local Ethereum chain "address": "0xd6f8ff0036d8b2088107902102f9415330868109", // Contract address "max_gas": 5000000 // Maximum gas limit for transactions } } ] } // other workflows can be added here... }, // ... the rest of the service manifest ``` ## Multi-workflow services A WAVS service can have one or multiple workflows. You can specify multiple workflows as objects in the service manifest. Each workflow can have a different trigger, component, and submission logic. All workflows in a service will share the same service manager and operator set. {/* todo: link above to service manager page. */} ``` { "workflows": { "workflow-uuid-1": { "trigger": { ... }, "component": { ... }, "submit": { ... } }, "workflow-uuid-2": { "trigger": { ... }, "component": { ... }, "submit": { ... } } // ... more workflows can be added here } } ``` ## Workflow isolation Each workflow execution is completely isolated with components running in separate WebAssembly environments. Each execution has its own memory space and components cannot directly access each other's memory or state. ## Sharing state WAVS services are designed to process data rather than store data. Data should be stored externally. To share data between workflows or components, the first workflow should submit data to an external system such as an onchain smart contract and the second workflow should read the data from the same system. ``` A: Trigger -> component -> onchain submission storage B: Trigger -> component (reads from A's onchain submission storage) -> onchain submission storage ``` 1. Workflow A submits data to a contract or external system 2. Workflow B reads data from the same contract or system Visit the [WAVS design considerations page](../design) for more information on best practices for WAVS services and storing data. ## Chaining workflows You can chain workflows together to create more complex execution flows. To have one workflow trigger another, set the event trigger of the second workflow to the onchain submission event of the first workflow. ``` { "workflows": { "workflow-uuid-1": { "trigger": { ... }, // trigger for first workflow "component": { ... }, // component for first workflow "submit": { ... } // submission logic for first workflow }, "workflow-uuid-2": { "trigger": { ... }, // trigger for second workflow is the onchain submission event of the first workflow "component": { ... }, // component for second workflow "submit": { ... } // submission logic for second workflow } } } ``` You can also chain different services together with this method by setting the trigger of the second service to the onchain submission event of the first service. ## Multichain services WAVS enables multichain services by allowing contract event or block height triggers on Cosmos or EVM chains (with more coming soon). This architecture lets you create cross-chain services that monitor events or block heights on one chain and submit the results to Ethereum. Visit the [Trigger page](./triggers) for more info. --- # Aggregator component URL: /handbook/components/aggregator Description: Building custom aggregator components that control on-chain submission As of WAVS v1.0.0, services that submit results on-chain must include a custom **aggregator component**. The aggregator component is a WASM component — like an operator component — but it runs inside the WAVS aggregator process instead of on each operator node. It decides *when* and *where* to submit the aggregated signatures and payload. ## How aggregator components differ from operator components | | Operator component | Aggregator component | |---|---|---| | **Macro** | `export_layer_trigger_world!` | `export_aggregator_world!` | | **Entry point** | `fn run(action: TriggerAction) -> Result, String>` | Three callbacks (see below) | | **Runs on** | Every registered operator node | The aggregator service | | **Purpose** | Execute business logic, produce a signed result | Decide when/where to submit the collected results | ## Entry points Aggregator components implement the `Guest` trait from the aggregator world bindings, which has three methods: ``` impl Guest for Component { /// Called when enough operator responses have been collected for a trigger event. /// Return a list of AggregatorActions — typically Submit or Timer. fn process_input(input: AggregatorInput) -> Result, String>; /// Called when a timer previously scheduled by process_input fires. /// Return Submit to proceed with on-chain submission, or an empty Vec to drop the event. fn handle_timer_callback(input: AggregatorInput) -> Result, String>; /// Called after the aggregator has attempted an on-chain submission. /// tx_result is Ok(tx_hash) on success or Err(message) on failure. fn handle_submit_callback( input: AggregatorInput, tx_result: Result, ) -> Result; } ``` ### `AggregatorInput` Each callback receives an `AggregatorInput` containing: - `trigger_action` — the original `TriggerAction` that started the workflow (same data available to the operator component) Use `host::get_event_id()` to retrieve the unique event ID for the current batch. ### `AggregatorAction` `process_input` and `handle_timer_callback` return `Vec`: ``` pub enum AggregatorAction { /// Submit the collected signatures and payload on-chain immediately. Submit(SubmitAction), /// Schedule handle_timer_callback to fire after a delay. Timer(TimerAction), } pub enum SubmitAction { Evm(EvmSubmitAction), Cosmos(CosmosSubmitAction), } pub struct EvmSubmitAction { pub chain: String, // Chain name as defined in wavs.toml (validated as ChainKey at runtime) pub address: EvmAddress, // Target service handler contract address pub gas_price: Option, // Optional gas price override (in wei) } pub struct TimerAction { pub delay: Duration, // How long to wait before calling handle_timer_callback } ``` ## Simple aggregator example The simple aggregator submits immediately when the threshold is reached. It reads the target chain and contract address from component config variables. ``` use world::{ host, wavs::aggregator::input::AggregatorInput, wavs::aggregator::output::{ AggregatorAction, EvmSubmitAction, SubmitAction, }, wavs::types::chain::{AnyTxHash, EvmAddress}, Guest, }; struct Component; impl Guest for Component { fn process_input(_input: AggregatorInput) -> Result, String> { let chain = host::config_var("chain") .ok_or("chain config variable is required")?; let service_handler_str = host::config_var("service_handler") .ok_or("service_handler config variable is required")?; let address: alloy_primitives::Address = service_handler_str .parse() .map_err(|e| format!("Failed to parse service handler address: {e}"))?; let submit_action = SubmitAction::Evm(EvmSubmitAction { chain, address: EvmAddress { raw_bytes: address.to_vec() }, gas_price: None, }); Ok(vec![AggregatorAction::Submit(submit_action)]) } fn handle_timer_callback(_input: AggregatorInput) -> Result, String> { Ok(Vec::new()) // Not used by simple aggregator } fn handle_submit_callback( _input: AggregatorInput, _tx_result: Result, ) -> Result { Ok(()) } } export_aggregator_world!(Component); ``` ## Timer-based aggregator example The timer aggregator defers submission: when `process_input` is called, it schedules a timer instead of submitting immediately. When the timer fires, `handle_timer_callback` validates the trigger data and then submits. This pattern is useful when you want to wait before submitting — for example, to batch multiple events or allow time for additional operator responses. ``` impl Guest for Component { fn process_input(_input: AggregatorInput) -> Result, String> { let timer_delay_secs: u64 = host::config_var("timer_delay_secs") .ok_or("timer_delay_secs config variable is required")? .parse() .map_err(|e| format!("Failed to parse timer_delay_secs: {e}"))?; // Schedule handle_timer_callback instead of submitting immediately Ok(vec![AggregatorAction::Timer(TimerAction { delay: Duration { secs: timer_delay_secs }, })]) } fn handle_timer_callback(input: AggregatorInput) -> Result, String> { let chain = host::config_var("chain") .ok_or("chain config variable is required")?; let service_handler_str = host::config_var("service_handler") .ok_or("service_handler config variable is required")?; let address: alloy_primitives::Address = service_handler_str .parse() .map_err(|e| format!("Failed to parse service handler address: {e}"))?; // Optionally validate the trigger data before submitting if !is_valid_trigger(input.trigger_action.data)? { return Ok(vec![]); // Drop the event — don't submit } Ok(vec![AggregatorAction::Submit(SubmitAction::Evm(EvmSubmitAction { chain, address: EvmAddress { raw_bytes: address.to_vec() }, gas_price: None, }))]) } fn handle_submit_callback( _input: AggregatorInput, tx_result: Result, ) -> Result { // Log result, write to KV store, etc. Ok(()) } } ``` ## Configuring the aggregator component in service.json The aggregator component is referenced in the `aggregator_component` field of your service manifest alongside the existing `aggregators` submission target: ```json service.json { "workflows": { "my-workflow": { "trigger": { "...": "..." }, "component": { "...": "..." }, "submit": { "aggregator": { "url": "http://127.0.0.1:8001" } }, "aggregators": [ { "evm": { "chain_name": "local", "address": "0xd6f8ff0036d8b2088107902102f9415330868109", "max_gas": 5000000 } } ], "aggregator_component": { "source": { "Registry": { "registry": { "digest": "...", "domain": "localhost:8090", "version": "0.1.0", "package": "example:simple-aggregator" } } }, "config": { "chain": "local", "service_handler": "0xd6f8ff0036d8b2088107902102f9415330868109" } } } } } ``` The `config` object is where you pass chain name and service handler address (and any other parameters your aggregator reads via `host::config_var`). ## Using the KV store in aggregator components Aggregator components have access to the same `wasi:keyvalue` sandboxed KV store as operator components. This is useful in `handle_submit_callback` to record submission results, or in `handle_timer_callback` to track state across timer firings. ``` use world::wasi::keyvalue::store; fn write_result(success: bool) -> Result { let bucket = store::open("submit-result").map_err(|e| e.to_string())?; bucket .set("success", if success { b"true" } else { b"false" }) .map_err(|e| e.to_string()) } ``` **KV store isolation:** The KV store is isolated by service — all components (operator and aggregator) within the same service share the same KV namespace. Components belonging to different services cannot access each other's KV data. ## Building the aggregator component Aggregator components are built the same way as operator components — compiled to WASM and uploaded to the registry. For example, using `wavs-cli`: ``` wavs-cli component upload --file target/wasm32-wasip1/release/simple_aggregator.wasm ``` After uploading, update the `aggregator_component.source.Registry.digest` in your `service.json` to the new digest printed by the upload command. --- # Blockchain interactions URL: /handbook/components/blockchain-interactions Description: Interacting with blockchains from WAVS components Components can interact with blockchains and smart contracts by using crates like [`wavs-wasi-utils`](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/). This page provides an overview of the dependencies and configuration needed to interact with Ethereum and other EVM-compatible chains. ### `wavs-wasi-utils` crate The `wavs-wasi-utils` crate provides a set of helpful functions for making HTTP requests and interacting with the blockchain. It also provides a macro for decoding trigger data for use in the component. Learn more in the [crate documentation](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/index.html). The `decode_event_log_data` macro is a utility for decoding Ethereum event logs from triggers into typed Rust events. It takes raw log data (topics and data) from the WAVS worker bindings and converts it into a Rust type that implements `SolEvent`. ## Dependencies The following dependencies are commonly required in your component's `Cargo.toml` for Ethereum interactions: ``` [dependencies] # Core WAVS blockchain functionality wit-bindgen-rt = { workspace = true, features = ["bitflags"] } # Required for WASI bindings wavs-wasi-utils = "1.0.0" # Blockchain interaction utilities wstd = "0.5.4" # WASI standard library # Alloy crates for Ethereum interaction alloy-sol-macro = { version = "1.0.0", features = ["json"] } # sol! macro for interfaces alloy-sol-types = "1.0.0" # ABI handling & type generation alloy-network = "1.0.0" # Network trait and Ethereum network type alloy-provider = { version = "1.0.0", default-features = false, features = ["rpc-api"] } # RPC provider alloy-rpc-types = "1.0.0" # RPC type definitions alloy-contract = "1.0.0" # Contract interaction utilities # Other useful crates anyhow = "1.0.98" # Error handling serde = { version = "1.0.219", features = ["derive"] } # Serialization/deserialization serde_json = "1.0.140" # JSON handling ``` Note: The `workspace = true` syntax can be used if your project is part of a workspace that defines these versions centrally. Otherwise, use the explicit versions shown above. ## Chain configuration Chain configurations are defined in the root `wavs.toml` file. This allows components to access RPC endpoints and chain IDs without hardcoding them. ```toml wavs.toml # Local / Testnet [default.chains.evm.local] chain_id = "31337" ws_endpoints = ["ws://localhost:8545"] # Array of WebSocket endpoints with automatic failover http_endpoint = "http://localhost:8545" poll_interval_ms = 7000 # Mainnet (multiple endpoints for redundancy) [default.chains.evm.ethereum] chain_id = "1" ws_endpoints = ["wss://eth.drpc.org", "wss://ethereum.publicnode.com"] # Failover to next on disconnect http_endpoint = "https://eth.drpc.org" ``` ### Sol! macro The `sol!` macro from `alloy-sol-macro` allows you to generate Rust types from Solidity interface files. You can write Solidity definitions (interfaces, structs, enums, custom errors, events, and function signatures) directly inside the `sol!` macro invocation in your Rust code. At compile time, the `sol!` macro parses that Solidity syntax and automatically generates the equivalent Rust types, structs, enums, and associated functions (like `abi_encode()` for calls or `abi_decode()` for return data/events) needed to interact with smart contracts based on those definitions. Required Dependencies: ``` [dependencies] alloy-sol-macro = { workspace = true } # For Solidity type generation alloy-sol-types = { workspace = true } # For ABI handling ``` Basic Pattern: ``` mod solidity { use alloy_sol_macro::sol; // Generate types from Solidity file sol!("../../src/interfaces/ITypes.sol"); // Or define types inline sol! { struct TriggerInfo { uint64 triggerId; bytes data; } event NewTrigger(TriggerInfo _triggerInfo); } } ``` In the template, the `sol!` macro is used in the `trigger.rs` component file to generate Rust types from the `ITypes.sol` file. ```rust trigger.rs pub mod solidity { use alloy_sol_macro::sol; pub use ITypes::*; // The objects here will be generated automatically into Rust types. // If you update the .sol file, you must re-run `cargo build` to see the changes. sol!("../../src/interfaces/ITypes.sol"); // Define a simple struct representing the function that encodes string input sol! { function addTrigger(string data) external; } } ``` The macro reads a Solidity interface file and generates corresponding Rust types and encoding/decoding functions. In the example above, it reads `ITypes.sol` which defines: - `NewTrigger` event - `TriggerInfo` struct - `DataWithId` struct More documentation on the `sol!` macro can be found at: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html ## Accessing configuration and provider WAVS provides host bindings to get the chain config for a given chain name in the wavs.toml file. The `new_evm_provider` from [`wavs-wasi-utils`](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/evm/fn.new_evm_provider.html) can be used to create a provider for a given chain. ```rust lib.rs use crate::bindings::host::get_evm_chain_config; use alloy_network::Ethereum; use alloy_provider::RootProvider; use wavs_wasi_utils::evm::new_evm_provider; // Get the chain config for a specific chain defined in wavs.toml let chain_config = get_evm_chain_config("local").unwrap(); // Create an Alloy provider instance using the HTTP endpoint let provider: RootProvider = new_evm_provider::( chain_config.http_endpoint.unwrap(), ); ``` ## Example: Querying NFT balance Here's an [example](https://github.com/Lay3rLabs/wavs-art/blob/main/components/autonomous-artist/src/evm.rs) demonstrating how to query the balance of an ERC721 NFT contract for a given owner address: ```rust lib.rs // Import required dependencies use crate::bindings::host::get_evm_chain_config; // WAVS host binding to get chain configuration use alloy_network::Ethereum; // Ethereum network type use alloy_provider::RootProvider; // Provider for making RPC calls use alloy_sol_macro::sol; // Macro for generating Solidity bindings use wavs_wasi_utils::evm::{ // WAVS utilities for EVM interaction alloy_primitives::{Address, U256}, // Ethereum primitive types new_evm_provider, // Function to create EVM provider }; use wstd::runtime::block_on; // Utility to run async code in sync context // Define the ERC721 interface using the sol! macro with #[sol(rpc)] for contract bindings sol! { #[sol(rpc)] interface IERC721 { function balanceOf(address owner) external view returns (uint256); } } // Function to check if an address owns any NFTs from a specific contract pub fn query_nft_ownership(address: Address, nft_contract: Address) -> Result { // block_on allows us to run async code in a synchronous function block_on(async move { // Get the chain configuration for the local network let chain_config = get_evm_chain_config("local").unwrap(); // Create a provider that will handle RPC communication let provider: RootProvider = new_evm_provider::( chain_config.http_endpoint.unwrap() ); // Create a contract instance bound to the provider let contract = IERC721::new(nft_contract, &provider); // Call balanceOf as a view call (no state changes, no gas) let balance: U256 = contract.balanceOf(address).call().await.map_err(|e| e.to_string())?; // Return true if the address owns at least one NFT Ok(balance > U256::ZERO) }) } ``` You can also use the `alloy-contract` crate to interact with smart contracts. See the [alloy-contract docs](https://crates.io/crates/alloy-contract) page for more information. See the [wavs-wasi-utils documentation](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/) and the [Alloy documentation](https://docs.rs/alloy/latest/alloy/) for more detailed information. ### Alloy ecosystem crates The Alloy ecosystem provides a comprehensive set of crates for Ethereum development: - [`alloy-primitives`](https://docs.rs/alloy-primitives/latest/alloy_primitives/): Core Ethereum types (`Address`, `U256`, `Bytes`, etc.) - [`alloy-provider`](https://docs.rs/alloy-provider/latest/alloy_provider/): Ethereum node interaction (RPC, WebSocket, batching) - [`alloy-network`](https://docs.rs/alloy-network/latest/alloy_network/): Network types and chain-specific functionality - [`alloy-sol-types`](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/): ABI handling and type generation - [`alloy-contract`](https://docs.rs/alloy-contract/latest/alloy_contract/): Contract interaction utilities ### Utility crates Essential utility crates for WAVS components: - [`wstd`](https://docs.rs/wstd/latest/wstd/): WASI standard library with `block_on` for async operations - [`serde`](https://docs.rs/serde/latest/serde/)/[`serde_json`](https://docs.rs/serde_json/latest/serde_json/): Data serialization and JSON handling - [`anyhow`](https://docs.rs/anyhow/latest/anyhow/): Error handling and propagation --- # Component overview URL: /handbook/components/component Description: Understanding WAVS service components and their structure 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: - [Go example](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/golang-evm-price-oracle) - [Typescript / JS example](https://github.com/Lay3rLabs/wavs-foundry-template/tree/main/components/js-evm-price-oracle) ## Component structure A basic component has three main parts: - Decoding incoming [trigger data](../triggers#trigger-lifecycle). - 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](../triggers#trigger-lifecycle) in two ways: 1. **On-chain events**: When triggered by an EVM event, the data comes through the `TriggerAction` with `TriggerData::EvmContractEvent`. 2. **Local testing**: When using `task wasi:exec` command in the template to test a component, the data comes through `TriggerData::Raw`. Here's how the example component handles both cases in `trigger.rs`: ``` // In trigger.rs pub fn decode_trigger_event(trigger_data: TriggerData) -> Result, 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 interface TriggerData::EvmContractEvent(TriggerDataEvmContractEvent { log, .. }) => { // Decode Ethereum event logs using the `decode_event_log_data!` macro let event: solidity::NewTrigger = decode_event_log_data!(log)?; let trigger_info = ::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 needed TriggerData::Raw(data) => Ok((0, data.clone(), Destination::CliOutput)), _ => Err(anyhow::anyhow!("Unsupported trigger data type")), } } pub mod solidity { // Define the Solidity types for the incoming trigger event using the `sol!` macro use 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 addTrigger function from the trigger contract sol! { function addTrigger(string data) external; } } ``` The component decodes the incoming event trigger data using the `decode_event_log_data!` macro from the [`wavs-wasi-utils` crate](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/macro.decode_event_log_data.html). The `sol!` macro from `alloy-sol-macro` is used to 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](./blockchain-interactions#sol-macro). ### 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.rs impl Guest for Component { fn run(action: TriggerAction) -> Result, String> { // 1. Decode the trigger data using the decode_trigger_event function from trigger.rs let (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 destination let output = match dest { // For on-chain submissions, the output is abi encoded using the encode_trigger_output function from trigger.rs Destination::Ethereum => Some(encode_trigger_output(trigger_id, &res)), // For local testing via wasi-exec, the output is returned as raw bytes Destination::CliOutput => Some(WasmResponse { payload: res.into(), ordering: None, }), }; Ok(output) } } ``` Components can contain any compatible logic, including [blockchain interactions](./blockchain-interactions), [network requests](./network-requests), off-chain computations, and more. To learn about the types of components that WAVS is best suited for, visit the [design considerations](../../design) 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 `task wasi:exec` locally in the template. ```rust lib.rs 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`. ```rust lib.rs 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 Ethereum pub fn encode_trigger_output(trigger_id: u64, output: impl AsRef) -> WasmResponse { WasmResponse { payload: solidity::DataWithId { triggerId: trigger_id, data: output.as_ref().to_vec().into(), } .abi_encode(), // ABI encode the struct for blockchain submission ordering: None, // Optional ordering parameter for transaction sequencing } } ``` The `run` function returns `Option` — return `Some(response)` to submit a result on-chain, or `None` to skip submission for this trigger. The `WasmResponse` struct has two fields: - `payload`: The output bytes returned by your component. The outer submission envelope is ABI-encoded by the runtime, but the payload itself can be arbitrary bytes — the submission contract determines how to interpret them. - `ordering`: Optional ordering parameter for transaction sequencing within a workflow (not yet implemented). The `Option` is submitted to WAVS which routes the response according to the workflow's submission logic. ## Component definition A component is defined in the [workflow](../workflows) object of the [service.json](../service) file. Below is an example of the different fields that can be defined in the component object. ```json service.json // ... 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](./variables) page. ## Registry WAVS uses a registry to store the WASM components. A service like [wa.dev](https://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}` --- # Network requests URL: /handbook/components/network-requests Description: Making HTTP requests from WAVS components Components can make network requests to external APIs using the [`wavs-wasi-utils` crate](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/index.html). To learn how to use variables like API keys in a component, visit the [Variables page](./variables). ### Dependencies The following dependencies are required for making HTTP requests from a component: ```toml Cargo.toml [dependencies] wavs-wasi-utils = "1.0.0" # HTTP utilities wstd = "0.5.4" # Runtime utilities (includes block_on) serde = { version = "1.0.219", features = ["derive"] } # Serialization serde_json = "1.0.140" # JSON handling ``` Since WASI components run in a synchronous environment but network requests are asynchronous, you can use `block_on` from the `wstd` crate to bridge this gap. The `block_on` function allows you to run async code within a synchronous context, which is essential for making HTTP requests in WAVS components. ### Making HTTP requests The `wavs-wasi-utils` crate provides several functions for making HTTP requests. See the [HTTP module documentation](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/http/index.html) for more details. ``` // Request functions http_request_get(url) // Creates a GET request http_request_post_json(url, data) // Creates a POST request with JSON data http_request_post_form(url, data) // Creates a POST request with form data // Response functions fetch_json(request) // Fetches and parses JSON response fetch_string(request) // Fetches response as string fetch_bytes(request) // Fetches raw response bytes ``` ### Example: GET request with headers Here's an example showing how to make a GET request with custom headers: ```rust lib.rs use wstd::runtime::block_on; use wstd::http::HeaderValue; use wavs_wasi_utils::http::{fetch_json, http_request_get}; use serde::{Deserialize, Serialize}; // Define response type with serde derive for automatic JSON parsing #[derive(Debug, Serialize, Deserialize)] struct ApiResponse { // ... your response fields } async fn make_request() -> Result { let url = "https://api.example.com/endpoint"; let mut req = http_request_get(&url).map_err(|e| e.to_string())?; // Set headers for API requests 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") ); // Use fetch_json to automatically parse the response let json: ApiResponse = fetch_json(req) .await .map_err(|e| e.to_string())?; Ok(json) } // Use block_on to handle async code in sync context fn process_data() -> Result { block_on(async move { make_request().await })? } ``` ### Example: POST request with JSON data For making POST requests with JSON data, you can use the `http_request_post_json` helper function: ```rust lib.rs use wstd::runtime::block_on; use wavs_wasi_utils::http::{fetch_json, http_request_post_json}; use serde::{Deserialize, Serialize}; // Define request and response types with serde derive #[derive(Debug, Serialize, Deserialize)] struct PostData { key1: String, key2: i32, } #[derive(Debug, Serialize, Deserialize)] struct PostResponse { // ... response fields } async fn make_post_request() -> Result { let url = "https://api.example.com/endpoint"; let post_data = PostData { key1: "value1".to_string(), key2: 42, }; // http_request_post_json automatically sets JSON headers let response: PostResponse = fetch_json( http_request_post_json(&url, &post_data)? ).await.map_err(|e| e.to_string())?; Ok(response) } fn process_data() -> Result { block_on(async move { make_post_request().await })? } ``` For more details, visit the [wavs-wasi-utils documentation](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/). --- # Utilities and crates URL: /handbook/components/utilities Description: Utilities and crates for WAVS component development {/* todo: verify content. Should we merge this into blockchain interactions? it may get too long. */} ### `wavs-wasi-utils` crate The `wavs-wasi-utils` crate provides a set of helpful functions for making HTTP requests and interacting with the blockchain. It also provides a macro for decoding trigger data for use in the component. Learn more in the [crate documentation](https://docs.rs/wavs-wasi-utils/latest/wavs_wasi_utils/index.html). The `decode_event_log_data` macro is a utility for decoding Ethereum event logs from triggers into typed Rust events. It takes raw log data (topics and data) from the WAVS worker bindings and converts it into a Rust type that implements `SolEvent`. ### Sol! macro The `sol!` macro from `alloy-sol-macro` allows you to generate Rust types from Solidity interface files. You can write Solidity definitions (interfaces, structs, enums, custom errors, events, and function signatures) directly inside the `sol!` macro invocation in your Rust code. At compile time, the `sol!` macro parses that Solidity syntax and automatically generates the equivalent Rust types, structs, enums, and associated functions (like `abi_encode()` for calls or `abi_decode()` for return data/events) needed to interact with smart contracts based on those definitions. Required Dependencies: ``` [dependencies] alloy-sol-macro = { workspace = true } # For Solidity type generation alloy-sol-types = { workspace = true } # For ABI handling ``` Basic Pattern: ``` mod solidity { use alloy_sol_macro::sol; // Generate types from Solidity file sol!("../../src/interfaces/ITypes.sol"); // Or define types inline sol! { struct TriggerInfo { uint64 triggerId; bytes data; } event NewTrigger(TriggerInfo _triggerInfo); } } ``` In the template, the `sol!` macro is used in the `trigger.rs` component file to generate Rust types from the `ITypes.sol` file. ```rust trigger.rs mod solidity { use alloy_sol_macro::sol; pub use ITypes::*; // The objects here will be generated automatically into Rust types. // If you update the .sol file, you must re-run `cargo build` to see the changes. sol!("../../src/interfaces/ITypes.sol"); } ``` The macro reads a Solidity interface file and generates corresponding Rust types and encoding/decoding functions. In the example above, it reads `ITypes.sol` which defines: - `NewTrigger` event - `TriggerInfo` struct - `DataWithId` struct More documentation on the `sol!` macro can be found at: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html ### `alloy-contract` crate {/* todo: verify this is correct */} The `alloy-contract` crate provides a high-level interface for interacting with Ethereum smart contracts. It offers a type-safe way to call contract functions and handle events, with features like automatic ABI encoding/decoding, gas estimation, and transaction management. The crate works with the `sol!` macro. You can use the `#[sol(rpc)]` attribute in components to generate contract bindings that work with WASI: ``` use alloy_primitives::{Address, U256}; use alloy_sol_types::sol; use alloy_provider::RootProvider; use alloy_network::Ethereum; use wavs_wasi_utils::evm::new_evm_provider; use crate::bindings::host::get_evm_chain_config; // Define your contract interface with RPC bindings sol! { #[sol(rpc)] contract ERC721 { function balanceOf(address owner) external view returns (uint256); function totalSupply() external view returns (uint256); function ownerOfTokenByIndex(uint256 index) external view returns (address); } } // Get chain config and create provider let chain_config = get_evm_chain_config("local")?; let provider: RootProvider = new_evm_provider::( chain_config.http_endpoint.unwrap() )?; // Create contract instance with the provider let contract = ERC721::new(contract_address, &provider); // Call contract functions directly let balance = contract.balanceOf(owner).call().await?; let total_supply = contract.totalSupply().call().await?; let owner = contract.ownerOfTokenByIndex(index).call().await?; ``` ### Alloy Ecosystem Crates The Alloy ecosystem provides a comprehensive set of crates for Ethereum development: - [`alloy-primitives`](https://docs.rs/alloy-primitives/latest/alloy_primitives/): Core Ethereum types (`Address`, `U256`, `Bytes`, etc.) - [`alloy-provider`](https://docs.rs/alloy-provider/latest/alloy_provider/): Ethereum node interaction (RPC, WebSocket, batching) - [`alloy-network`](https://docs.rs/alloy-network/latest/alloy_network/): Network types and chain-specific functionality - [`alloy-sol-types`](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/): ABI handling and type generation ### Utility Crates Essential utility crates for WAVS components: - [`wstd`](https://docs.rs/wstd/latest/wstd/): WASI standard library with `block_on` for async operations - [`serde`](https://docs.rs/serde/latest/serde/)/[`serde_json`](https://docs.rs/serde_json/latest/serde_json/): Data serialization and JSON handling - [`anyhow`](https://docs.rs/anyhow/latest/anyhow/): Error handling and propagation --- # Variables URL: /handbook/components/variables Description: Managing configuration variables in WAVS components Components can be configured with two types of variables: ## Public variables These variables can be used for non-sensitive information that can be viewed publicly. These variables are set in the `config` field of a service manifest. All config values are stored as strings, even for numbers. To add public variables: 1. Add the public variables to the `config` field in the service manifest: ``` "component": { "config": { "api_endpoint": "https://api.example.com", // Access using host::config_var() "max_retries": "3" // Config values are always strings } } ``` 2. Access them in the component using `host::config_var()`: ``` let value = host::config_var("api_endpoint"); ``` ## Environment keys Environment keys are private and can be used for sensitive data like API keys. These variables are set by operators in their environment and are not viewable by anyone. These variables must be prefixed with `WAVS_ENV_`. Each operator must set these variables in their environment before deploying the service. WAVS validates that all environment variables are set before allowing the service to run. To add private variables: 1. Create a new `.env` file in WAVS template: ``` # copy the example file cp .env.example .env ``` Variables can also be set in your `~/.bashrc`, `~/.zshrc`, or `~/.profile` files. 2. Set the environment variable in your `.env` file: ``` # .env file WAVS_ENV_MY_API_KEY=your_secret_key_here ``` 3. Access the environment key from a component: ``` let api_key = std::env::var("WAVS_ENV_MY_API_KEY")?; ``` 4. Before deploying a service, add the environment key to the `env_keys` array in the service manifest: ``` "component": { "env_keys": [ "WAVS_ENV_API_KEY" // Environment variables the component can access. Must be prefixed with WAVS_ENV_ ] } ``` ## Local Execution When running components locally (raw), use the `--config` flag to set values in a KEY=VALUE format, comma-separated: `--config a=1,b=2`. ``` wavs-cli exec --component --input --config api_endpoint=https://api.example.com ``` --- # LLM docs URL: /resources/llms Description: Access WAVS documentation in formats optimized for AI tools and integration. **AI components:** To learn how to create one-shot components with AI, visit the [AI-powered component creation page](/handbook/ai) The LLM text format presents documentation in a clean, plain text format optimized for large language models (LLMs) like Claude, ChatGPT, and others. ## llms.txt The `llms.txt` format is a structured index of documentation pages organized by sections, including page titles, URLs and descriptions. This format is ideal for AI assistants to understand the documentation structure without processing the full content. [https://docs.wavs.xyz/llms.txt](https://docs.wavs.xyz/llms.txt) ``` curl https://docs.wavs.xyz/llms.txt ``` ## llms-full.txt The `llms-full.txt` format returns all documentation pages as a single text document. [https://docs.wavs.xyz/llms-full.txt](https://docs.wavs.xyz/llms-full.txt) ``` curl https://docs.wavs.xyz/llms-full.txt ``` ## Markdown Format Get any page as standard Markdown by appending `.md` to its URL. ``` curl https://docs.wavs.xyz/path/to/page.md ``` Examples: - `/overview.md` - Overview page as Markdown - `/tutorial/1-overview.md` - Tutorial introduction as Markdown - `/handbook/service.md` - Service handbook as Markdown ## WAVS foundry template An llm-ingestible full markdown version of the [WAVS foundry template](https://github.com/Lay3rLabs/wavs-foundry-template) is available at [https://docs.wavs.xyz/wavs-foundry-template.md](https://docs.wavs.xyz/wavs-foundry-template.md). ``` curl https://docs.wavs.xyz/wavs-foundry-template.md ``` ## WAVS-WASI-utils crate docs An llm-ingestible full markdown version of the WAVS-WASI-Utils docs is available at [https://docs.wavs.xyz/wavs-wasi-utils.md](https://docs.wavs.xyz/wavs-wasi-utils.md). ``` curl https://docs.wavs.xyz/wavs-wasi-utils.md ``` ## Search API Search the documentation programmatically using the search API endpoint. ``` curl "https://docs.wavs.xyz/api/search-custom?q=your_query" ``` Examples: - Search for "component": `https://docs.wavs.xyz/api/search-custom?q=component` The search API returns JSON results with page titles, URLs, and content snippets. ---