Layer LogoLayer AVS Docs
Examples

Oracle Service example

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

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 history
let mut history = price_history::PriceHistory::read()?;
// get current time in secs
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("failed to get current time")
.as_secs();
// record latest price
history.record_latest_price(now, price)?;
// calculate average price over the past hour
let 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 list
self.btcusd_prices.push_front((now, price));
self.btcusd_prices.truncate(1000);
// write price history
std::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

On this page