Oracle Service example (for reference only)
This example is not currently live on the hacknet, but is provided here as an example. For a hands-on guide for creating an AVS from scratch, follow the getting started guide.
Introduction
In this example, we are going to walkthrough creating a Oracle for the average price of BTCUSD
over the past hour.
We will create an application that periodically queries the CoinGecko API for the latest price, saves
it, and then publishes to the chain the computed average price.
This guide recreates the oracle-example
found here.
To get setup to create a new app, please follow the prerequisites instructions here.
Setup a New Project
This example is not currently live on the hacknet, but is provided here as an example. For a hands-on guide for creating an AVS from scratch, follow the getting started guide.
To create an new Wasm component for use with Layer's Wasmatic, let's use cargo component
to scaffold a new project with a task queue trigger. Feel free to choose a different project
name then "my-oracle-example".
cargo component new --lib --target lay3r:avs/task-queue my-oracle-example && cd my-oracle-example
Let's do the first build to generate the src/bindings.rs
file. Afterwards, you can do the
familiar cargo
commands such as cargo test
and cargo clippy
. It may be helpful to inspect
src/bindings.rs
during development to see the type information for producing the Wasm component.
cargo component build
In order to make outgoing HTTP requests easily, add the layer-wasi
crate as dependency.
cargo add --git https://github.com/Lay3rLabs/avs-toolkit layer-wasi
Also, let's add a few other dependencies that will help.
cargo add anyhow serde-json serde --features serde/derive
Generated Scaffolding
When you scaffolded the new project, src/lib.rs
has something like this:
struct Component;impl Guest for Component {fn run_task(request: TaskQueueInput) -> Output {unimplemented!()}}
You can inspect the bindings
types for creating this Wasm component in the generated file
src/bindings.rs
.
The important bit is the run_task
method. That's the exported method that is the entry point
for execution. For this example, we won't use the request
input argument, so
you prefix it with _
to tell Rust that we are intentionally not using it.
Implementation
The complete source code for the example is found here.
Let's add some imports for the src/lib.rs
:
use layer_wasi::{block_on, Reactor};use serde::Serialize;use std::time::{SystemTime, UNIX_EPOCH};
The layer_wasi::{block_on, Reactor}
are re-exports from the
wstd
crate that enables
us to use an async
runtime inside of the Wasm component.
Replace the unimplemented()
in the run_task()
method with:
block_on(get_avg_btc)
And let's create a new async fn get_avg_btc
for this to call.
/// Record the latest BTCUSD price and return the JSON serialized result to write to the chain.async fn get_avg_btc(reactor: Reactor) -> Result<Vec<u8>, String> {let api_key = std::env::var("API_KEY").or(Err("missing env var `API_KEY`".to_string()))?;let price = coin_gecko::get_btc_usd_price(&reactor, &api_key).await.map_err(|err| err.to_string())?.ok_or("invalid response from coin gecko API")?;// read previous price historylet mut history = price_history::PriceHistory::read()?;// get current time in secslet now = SystemTime::now().duration_since(UNIX_EPOCH).expect("failed to get current time").as_secs();// record latest pricehistory.record_latest_price(now, price)?;// calculate average price over the past hourlet avg_last_hour = history.average(now - 3600);CalculatedPrices {price: avg_last_hour.price.to_string(),}.to_json()}/// The returned result.#[derive(Serialize, Debug)]#[serde(rename_all = "camelCase")]struct CalculatedPrices {price: String,}impl CalculatedPrices {/// Serialize to JSON.fn to_json(&self) -> Result<Vec<u8>, String> {serde_json::to_vec(&self).map_err(|err| err.to_string())}}
As you can see, we are getting the API_KEY
environment variable as configuration that we will use
for the CoinGecko API HTTP request. The function returns a serialized JSON in the form of
{"price": "100.00"}
We will need to implement a couple modules and functions that we are calling. Add two modules to the top
of the src/lib.rs
:
mod coin_gecko;mod price_history;
Next let's implement a way for us to persist a price history, retrieve it, and compute
the average price. Create a new file and with your text editor as src/price_history.rs
:
use serde::{Deserialize, Serialize};use std::collections::VecDeque;const PRICE_HISTORY_FILE_PATH: &str = "price_history.json";#[derive(Serialize, Debug, PartialEq)]#[serde(rename_all = "camelCase")]pub struct AveragePrice {pub price: f32,pub count: usize,}#[derive(Deserialize, Serialize, Debug, Default)]#[serde(rename_all = "camelCase")]pub struct PriceHistory {pub btcusd_prices: VecDeque<(u64, f32)>,}impl PriceHistory {/// Read price history from the file system or initialize empty.pub fn read() -> Result<Self, String> {match std::fs::read(PRICE_HISTORY_FILE_PATH) {Ok(bytes) => {serde_json::from_slice::<PriceHistory>(&bytes).map_err(|err| err.to_string())}Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Default::default()),Err(err) => Err(err.to_string()),}}/// Record latest price to price history and truncate to max of 1000 of price history./// `now` is the specified time in UNIX Epoch seconds.////// Updates the price history on the file system.pub fn record_latest_price(&mut self, now: u64, price: f32) -> Result<(), String> {// add to the front of the listself.btcusd_prices.push_front((now, price));self.btcusd_prices.truncate(1000);// write price historystd::fs::write(PRICE_HISTORY_FILE_PATH,serde_json::to_vec(&self).map_err(|err| err.to_string())?,).map_err(|err| err.to_string())}/// Calculate the average price since the specified time in UNIX Epoch seconds.pub fn average(&self, since_time_secs: u64) -> AveragePrice {let mut sum = 0f64;let mut count = 0;for (t, p) in self.btcusd_prices.iter() {if t >= &since_time_secs {sum += *p as f64;count += 1;} else {break;}}AveragePrice {price: (sum / (count as f64)) as f32,count,}}}
Lastly, let's add one more file and implement the HTTP calls to retrieve the BTCUSD price from
the CoinGecko API. Create a new file and with your text editor as src/coin_gecko.rs
:
use layer_wasi::{Reactor, Request, WasiPollable};use serde::Deserialize;use std::collections::HashMap;#[derive(Deserialize, Debug)]pub struct CoinInfo {pub value: f32,}#[derive(Deserialize, Debug)]pub struct CoinGeckoResponse {pub rates: HashMap<String, CoinInfo>,}impl CoinGeckoResponse {fn btc_usd(&self) -> Option<f32> {self.rates.get("usd").map(|info| info.value)}}pub async fn get_btc_usd_price(reactor: &Reactor, api_key: &str) -> Result<Option<f32>, String> {let mut req = Request::get("https://api.coingecko.com/api/v3/exchange_rates")?;req.headers = vec![("x-cg-pro-api-key".to_string(), api_key.to_owned())];let res = reactor.send(req).await?;match res.status {200 => res.json::<CoinGeckoResponse>().map(|info| info.btc_usd()),429 => Err("rate limited, price unavailable".to_string()),status => Err(format!("unexpected status code: {status}")),}}
Deploying
First, let's do a release build of the component:
cargo component build --release
Upload the compiled Wasm component to the Wasmatic node using the avs-toolkit-cli
CLI tool
(if you don't have it already, go check the chapter Rust CLI
).
Assign a unique name, as it is how your application is going to be distinguished. The examples below assume
the assigned name is my-oracle-example
.
You'll also need to use the task address that was created when you deployed your contract. If you forgot how to access it, you can find it in the section on contracts.
This example integrates with the CoinGecko API to retrieve the latest BTCUSD price. You will need to sign up
and provide an API key, see instructions.
Replace the <YOUR-API-KEY>
below with your key.
avs-toolkit-cli wasmatic deploy --name my-oracle-example \--wasm-source ./target/wasm32-wasip1/release/my_oracle_example.wasm \--testable \--envs "API_KEY=<YOUR-API-KEY>" \--task <TASK-ADDRESS>
Test Executions
To test and execute locally, run the following with the provided environment variable for the app:
avs-toolkit-cli wasmatic run \--wasm-source ./target/wasm32-wasip1/release/my_oracle_example.wasm \--envs "API_KEY=<YOUR-API-KEY>"
If you would like local execution of the app to maintain its file system state across multiple local executions and available for easy inspection,
provide --dir
flag with the directory path for the app to store its state. For example, using ./app-data
dir in the current working directory.
If omitted, the app will use a temporary directory that will be cleaned up after execution completes.
avs-toolkit-cli wasmatic run \--wasm-source ./target/wasm32-wasip1/release/my_oracle_example.wasm \--envs "API_KEY=<YOUR-API-KEY>" \--dir app-data
To test the deployed application on the Wasmatic node, you can use the test endpoint. The server responds with the output of the applicaton without sending the result to the chain.
avs-toolkit-cli wasmatic test --name my-oracle-example