๐Ÿ‘‹Getting Started: Price Feed

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 seda-request-starter-kit. 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.

To get started, clone the repository and install the required dependencies:

git clone git@github.com:sedaprotocol/seda-request-starter-kit.git
cd seda-request-starter-kit
bun install

Building the Oracle Program

The PriceFeed Oracle Program is divided into 2 phases, as can be seen inassembly/index.ts:

  1. Execution: it fetches the price of an asset pair (e.g., BTC-USDT) from an external source and processes this data.

  2. Tally: it calculates the median among all executions reports.

This will illustrate 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 theassemblyfolder, where index.ts serves as the entry point for your Oracle Program.

assembly/index.ts
import { OracleProgram } from "@seda-protocol/as-sdk/assembly";

class PriceFeed extends OracleProgram {
  execution(): void {
    executionPhase();
  }

  tally(): void {
    tallyPhase();
  }
}

// Runs the PriceFeed oracle program by executing both phases.
new PriceFeed().run();

This setup initializes the PriceFeed Oracle Program, invoking 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 the price for a specified asset pair from a given data source.

Read Inputs

The user triggering the Data Request can specify a set of inputs that can be used in the data request to add more dynamic execution and templating. In the following example we start by reading the input parameters using Process.getInputs():

import { Console, Process } from "@seda-protocol/as-sdk/assembly";

// ...

function executionPhase(): void {
  // Retrieve the input parameters for the data request (DR).
  // Expected to be in the format "symbolA-symbolB" (e.g., "BTC-USDT").
  const drInputsRaw = Process.getInputs().toUtf8String();

  // Log the asset pair being fetched as part of the Execution Standard Out.
  Console.log(`Fetching price for pair: ${drInputsRaw}`);

  // Split the input string into symbolA and symbolB.
  // Example: "ETH-USDC" will be split into "ETH" and "USDC".
  const drInputs = drInputsRaw.split("-");
  const symbolA = drInputs[0];
  const symbolB = drInputs[1];
  
  // ...
}

In this example, we assume the input is a UTF-8 encoded string. We log the asset pair being fetched to include this information in the data request execution. The input string is then split to extract the symbols needed for this specific API.

Fetch and Compute Data

Next, let's add the logic to fetch data from an external API and perform some calculations:

import { httpFetch } from "@seda-protocol/as-sdk/assembly";

// API response structure for the price feed
@json
class PriceFeedResponse {
  price!: string;
}

function executionPhase(): void {
  // ...
  
  // Make an HTTP request to a price feed API to get the price for the symbol pair.
  // The URL is dynamically constructed using the provided symbols (e.g., ETHUSDC).
  const response = httpFetch(
    `https://api.binance.com/api/v3/ticker/price?symbol=${symbolA.toUpperCase()}${symbolB.toUpperCase()}`
  );
  
  // Parse the API response as defined earlier.
  const data = response.bytes.toJSON<PriceFeedResponse>();
  
  // Convert to integer (and multiply by 1e6 to avoid losing precision).
  const priceFloat = f32.parse(data.price);
  const result = u128.from(priceFloat * 1000000);

  // ...
}

Here's a breakdown of the logic:

  1. Fetch Data: Perform an HTTP request to fetch the price for the asset pair using the httpFetch function. The URL is constructed dynamically based on the asset symbols.

  2. Parse Response: The API response is parsed into a PriceFeedResponse object. This allows us to easily extract the price data from the response.

  3. Perform Calculations: The fetched price, which is a floating-point number, is multiplied by 1e6 to avoid precision loss and then converted into a u128 integer.

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 Bytes.fromNumber.

If there are any errors during the process, they can be handled using the Process.error() call. Additionally, Console can be used to log errors for debugging purposes.

import { Console, Process, httpFetch } from "@seda-protocol/as-sdk/assembly";

// API response structure for the price feed
@json
class PriceFeedResponse {
  price!: string;
}

function executionPhase(): void {
  // ...
  
  // Make an HTTP request to a price feed API to get the price for the symbol pair.
  const response = httpFetch(
    `https://api.binance.com/api/v3/ticker/price?symbol=${symbolA.toUpperCase()}${symbolB.toUpperCase()}`
  );

  // Check if the HTTP request was successfully fulfilled.
  if (!response.ok) {
    // Handle the case where the HTTP request failed or was rejected.
    Console.error(
      `HTTP Response was rejected: ${response.status.toString()} - ${response.bytes.toUtf8String()}`
    );
    // Report the failure to the SEDA network with an error code of 1.
    Process.error(Bytes.fromUtf8String("Error while fetching price feed"));
  }

  // Parse the API response as defined earlier.
  const data = response.bytes.toJSON<PriceFeedResponse>();

  // Convert to integer (and multiply by 1e6 to avoid losing precision).
  const priceFloat = f32.parse(data.price);
  if (isNaN(priceFloat)) {
    // Report the failure to the SEDA network with an error code of 1.
    Process.error(Bytes.fromUtf8String(`Error while parsing price data: ${data.price}`));
  }
  const result = u128.from(priceFloat * 1000000);

  // Report the successful result back to the SEDA network.
  Process.success(Bytes.fromNumber<u128>(result));
}

Here's a summary of what's happening:

  1. Error Handling: We check if the HTTP request was successful. If not, we log the error and use Process.error to report the issue to the SEDA network.

  2. Parse and Validate: We parse the API response into a PriceFeedResponse object and ensure that the price data can be correctly converted to a float.

  3. Report the Result: If everything is successful, we multiply the float by 1e6 to convert it into an integer and use Process.success to report the result back to the SEDA network.

This completes the execution phase, ensuring that errors are handled gracefully and results are accurately processed.

Step 3: Tally Phase

The tally phase is where the results collected during the execution phase are aggregated to reach a consensus. This phase processes the data gathered by multiple Overlay Nodes to produce a single result that can be returned to the blockchain.

In this example, we calculate the median price from the results obtained during the execution phase to provide a final output. Here's how we can implement this logic:

import { Bytes, Process, Tally, u128 } from "@seda-protocol/as-sdk/assembly";

export function tallyPhase(): void {
  // Tally inputs can be retrieved from Process.getInputs(), though it is unused in this example.
  // const tallyInputs = Process.getInputs();

  // Retrieve consensus reveals from the tally phase.
  const reveals = Tally.getReveals();
  const prices: u128[] = [];

  // Iterate over each reveal, parse its content as an unsigned integer (u64), and store it in the prices array.
  for (let i = 0; i < reveals.length; i++) {
    const price = reveals[i].reveal.toU128();
    prices.push(price);
  }

  if (prices.length > 0) {
    // If there are valid prices revealed, calculate the median price from price reports.
    const finalPrice = 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(Bytes.fromNumber<u128>(finalPrice, true));
  } else {
    // If no valid prices were revealed, report an error indicating no consensus.
    Process.error(Bytes.fromUtf8String("No consensus among revealed results"));
  }
}

function median(numbers: u128[]): u128 {
    // ...
}

Explanation:

  1. Retrieve Results: The tallyPhase gathers the execution results using Tally.getReveals(). These results are parsed into unsigned integers (u128) and stored for further analysis.

  2. Consensus Check: Before proceeding, it verifies that there are enough price data points from the execution phase to move forward with aggregation.

  3. 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.

  4. Result Reporting: If the median calculation is successful, the final result is reported using Process.success(). If no valid results are available or consensus is not achieved, an error is logged using Process.error().

Now that the Oracle Program is implemented, we can generate the WebAssembly artifacts. This process compiles your code and places the resulting .wasm files in the build directory. After building, you'll have the necessary artifacts to deploy your Oracle Program on the SEDA network.

To build the Oracle Program, run the following command:

bun run build

This process completes the PriceFeed Oracle Program. In the next chapters we will be showcasing how to deploy and trigger Data Requests.

Last updated