This section explains the basics of building a SEDA Oracle program and how to use it from any supported network using the PriceFeed example.
Quickstart
The easiest way to start building a SEDA Oracle Program is by using the . This starter kit offers a streamlined setup process and comes with the essential tools and structure to help you create and deploy your first Oracle program on the SEDA network.
Requirements
Before getting started, make sure your environment has the following tools installed:
: Used for package management, running scripts, and building the TypeScript project.
: Required for compiling and running the Oracle Program.
Alternatively, you can use the pre-configured included in the code repository to skip manual setup. This includes all necessary tools and dependencies for development.
Setup
To get started, clone the repository and install the required dependencies:
git clone https://github.com/sedaprotocol/seda-starter-kit.git
cd seda-starter-kit
bun install
Building the Oracle Program
The PriceFeed Oracle Program is divided into two phases:
Execution: Fetches the price of an asset pair (e.g., BTC-USDT) from an external source and processes the data.
Tally: Calculates the median of all execution reports.
This illustrates the basic mechanics of how data is processed and aggregated within the SEDA network.
Step 1: Entrypoint
To begin, the Oracle Program logic lives in the src folder, where main.rs serves as the entry point for your Oracle Program.
src/main.rs
use execution_phase::execution_phase;
use seda_sdk_rs::oracle_program;
use tally_phase::tally_phase;
mod execution_phase;
mod tally_phase;
#[oracle_program]
impl PriceFeed {
fn execute() {
execution_phase().unwrap();
}
fn tally() {
tally_phase().unwrap();
}
}
This setup initializes the PriceFeed Oracle program and links the execution and tally phases that make up the data request.
Step 2: Execution Phase
The execution phase is where the Oracle Program fetches data and performs computation on the fetched data. In this case, it fetches and computes data for the specified asset pair.
Read Inputs
Users can specify input parameters in the data request to support dynamic execution. Here's how we read those inputs using Process::get_inputs():
src/execution_phase.rs
pub fn execution_phase() -> Result<()> {
// Retrieve the input parameters for the data request (DR).
// Expected to be in the format "symbolA-symbolB" (e.g., "BTC-USDT").
let dr_inputs_raw = String::from_utf8(Process::get_inputs())?;
// Log the asset pair being fetched as part of the Execution Standard Out.
log!("Fetching price for pair: {}", dr_inputs_raw);
// Split the input string into symbolA and symbolB.
// Example: "ETH-USDC" will be split into "ETH" and "USDC".
let dr_inputs: Vec<&str> = dr_inputs_raw.split("-").collect();
let symbol_a = dr_inputs.first().expect("format should be tokenA-tokenB");
let symbol_b = dr_inputs.get(1).expect("format should be tokenA-tokenB");
// ...
}
In this example, we assume the input is a UTF-8 encoded string. The input is split to extract the individual symbols required by the API.
We use the log! macro instead of functions like print! because it is more gas-efficient.
Fetch and Compute Data
Next, let's add the logic to fetch data from an API and perform calculations:
src/execution_phase.rs
use anyhow::Result;
use seda_sdk_rs::{elog, http_fetch, log, Process};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct PriceFeedResponse {
price: String,
}
pub fn execution_phase() -> Result<()> {
// ....
let response = http_fetch(
format!(
"https://api.binance.com/api/v3/ticker/price?symbol={}{}",
symbol_a.to_uppercase(),
symbol_b.to_uppercase()
),
None,
);
// Check if the HTTP request was successfully fulfilled.
if !response.is_ok() {
// Handle the case where the HTTP request failed or was rejected.
elog!(
"HTTP Response was rejected: {} - {}",
response.status,
String::from_utf8(response.bytes)?
);
// Report the failure to the SEDA network with an error code of 1.
Process::error("Error while fetching price feed".as_bytes());
return Ok(());
}
// Parse the API response as defined earlier.
let data = serde_json::from_slice::<PriceFeedResponse>(&response.bytes)?;
// Convert to integer (and multiply by 1e6 to avoid losing precision).
let price: f32 = data.price.parse()?;
log!("Fetched price: {}", price);
let result = (price * 1000000f32) as u128;
log!("Reporting: {}", result);
// ....
}
Breakdown of the logic:
Fetch Data: An HTTP request retrieves the price using http_fetch. The URL is constructed dynamically from asset symbols.
Parse Response: The API response is deserialized into a PriceFeedResponse struct.
Perform Calculations: The price (a floating-point number) is multiplied by 1e6 to preserve precision and cast to a u128.
For more information about fetching external data, please refer to the detailed guide Fetching Open Data.
Report Results & Error Handling
Finally, we need to report the result and indicate that the Process has ended successfully using the Process::success() call. Since Process only works with Bytes, we convert our result to bytes using result.to_le_bytes().
If an error occurs at any point, it can be handled with Process::error().
For debugging purposes, you can use the log!() and elog!() macros to record informational messages and errors. These macros are preferred over alternatives like print!() or eprint!() because they are more gas-efficient in the SEDA VM.
src/execution_phase.rs
use anyhow::Result;
use seda_sdk_rs::{elog, http_fetch, log, Process};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct PriceFeedResponse {
price: String,
}
pub fn execution_phase() -> Result<()> {
// ...
// Check if the HTTP request was successfully fulfilled.
if !response.is_ok() {
// Handle the case where the HTTP request failed or was rejected.
elog!(
"HTTP Response was rejected: {} - {}",
response.status,
String::from_utf8(response.bytes)?
);
// Report the failure to the SEDA network with an error code of 1.
Process::error("Error while fetching price feed".as_bytes());
return Ok(());
}
// Parse the API response as defined earlier.
let data = serde_json::from_slice::<PriceFeedResponse>(&response.bytes)?;
// Convert to integer (and multiply by 1e6 to avoid losing precision).
let price: f32 = data.price.parse()?;
log!("Fetched price: {}", price);
let result = (price * 1000000f32) as u128;
log!("Reporting: {}", result);
// Report the successful result back to the SEDA network.
Process::success(&result.to_le_bytes());
}
Summary of what's happening:
Error Handling: The program checks whether the HTTP request was successful. If not, it logs the error and uses Process::error() to report the issue to the SEDA network.
Parse and Validate: The API response is parsed into a PriceFeedResponse object, and the price string is safely converted into a floating-point number.
Report the Result: If everything succeeds, the float is multiplied by 1e6 to preserve precision, converted to a u128, and submitted using Process::success().
This completes the execution phase, ensuring that errors are gracefully handled and results are accurately processed.
Step 3: Tally Phase
The tally phase aggregates results collected during the execution phase to reach consensus. This data, gathered by multiple Overlay Nodes, is processed into a single output value that can be returned to the blockchain.
In this example, we calculate the median price from all valid results obtained during execution.
src/tally_phase.rs
use anyhow::Result;
use seda_sdk_rs::{elog, get_reveals, log, Process};
/**
* Executes the tally phase within the SEDA network.
* This phase aggregates the results (e.g., price data) revealed during the execution phase,
* calculates the median value, and submits it as the final result.
* Note: The number of reveals depends on the replication factor set in the data request parameters.
*/
pub fn tally_phase() -> Result<()> {
// Tally inputs can be retrieved from Process.getInputs(), though it is unused in this example.
// let tally_inputs = Process::get_inputs();
// Retrieve consensus reveals from the tally phase.
let reveals = get_reveals()?;
let mut prices: Vec<u128> = Vec::new();
// Iterate over each reveal, parse its content as an unsigned integer (u128), and store it in the prices array.
for reveal in reveals {
let price_bytes_slice: [u8; 16] = match reveal.body.reveal.try_into() {
Ok(value) => value,
Err(_err) => {
// We should always handle a reveal body with care and not exit/panic when parsing went wrong
// It's better to skip that reveal
elog!("Reveal body could not be casted to u128");
continue;
}
};
let price = u128::from_le_bytes(price_bytes_slice);
log!("Received price: {}", price);
prices.push(price);
}
if prices.is_empty() {
// If no valid prices were revealed, report an error indicating no consensus.
Process::error("No consensus among revealed results".as_bytes());
return Ok(());
}
// If there are valid prices revealed, calculate the median price from price reports.
let final_price = median(prices);
// Report the successful result in the tally phase, encoding the result as bytes.
// Encoding result with big endian to decode from EVM contracts.
Process::success(&final_price.to_be_bytes());
Ok(())
}
fn median(mut nums: Vec<u128>) -> u128 {
// ...
}
Explanation:
Retrieve Results: The tally_phase function gathers execution results using get_reveals(). Each result is parsed into a u128 integer.
Consensus Check: If no valid results are found, an error is reported using Process::error(), indicating that consensus was not reached.
Aggregate Data: The parsed prices are used to calculate the median, a robust statistical measure that minimizes the impact of outliers and provides a reliable data representation.
Result Reporting: The final median price is submitted to the SEDA network using Process::success(), encoded in big-endian format for compatibility with EVM chains.
Building the Oracle Program
Now that the Oracle program is implemented, you can compile it to generate the WebAssembly artifacts. This process places the resulting .wasm files in the build directory.
To build the Oracle program, run:
bun run build
After building, you'll have the necessary artifacts to deploy your Oracle Program on the SEDA network.
In the next chapters we will be showcasing how to deploy and trigger Data Requests.