Course Content

Chapter 0: Introduction



Chapter 2: How to create an oracle script




Chapter 2

How to create an oracle script

Estimated time: 2 hr 26 mins


Writing the oracle script

In the last chapter, we discussed how an oracle script works and what sort of code we need to implement to create one. Luckily, Band Protocol provides a Owasm Library, owasm-kit, which contains the oei module, which defines a set of functions used by the OEI and the ext module, which is an extension module that contains convenient wrapper functions that allow the user to calculate the mean, median and majority results from the validator's reported results amongst other things.

While an oracle script can be any type of WebAssembly binary, in this course, we'll only be detailing how to write an oracle script in Rust using the modules provided in owasm-kit and obi

Inputs and outputs

When creating an oracle script, we need to define the input and output types that the oracle script will recieve and output. We also need to be able to serialize and deserialize the data when interacting with BandChain using Oracle Binary Encoding (OBI).

When creating an oracle script, the oracle script's input and output needs to be defined. As the oracle script interacts with BandChain, we also need to be able to serialize and deserialize the data coming into and out of BandChain.

In order to serialize and deserialize the data we can use OBI. BandChain's Rust implementation for OBI contains derive macros OBIEncode and OBIDecode which can be derived by the output and input structs respectively in order to implement the required methods that will be used by the prepare_entry_point and execute_entry_point macros. These macros which will later be explained in more detail.

The example below shows an input and outputs struct of an oracle script that takes quote_currency as a String and outputs its price as u64.

// The input struct derives the OBIDecode and OBISchema trait in order to derive
// the serialized data coming in from BandChain
#[derive(OBIDecode, OBISchema)]
struct Input {
    quote_currency: String,
}

// The output struct derives the OBIEncode and OBISchema trait in order to
// serialize the data that is to be sent back to BandChain
#[derive(OBIEncode, OBISchema)]
struct Output {
    price: u64
}

Preparation phase

Now that we've defined our inputs and outputs, we need to instruct our oracle script to call the data source we want to get the data required.

In order to do this, we can implement a function called prepare_impl which will utilize the function ask_external_data(), which is contained in owasm-kit::oei, to send out a request to the specified data sources to retrieve the data required.

The parameters of ask_external_data() are:

ParametersTypeDescription
external_idi64A unique identifier set within the oracle script which is used to identify a certain request when asking for the results
data_source_idi64The ID of the data source to call
calldata&[u8]The calldata to be sent to the data source in bytes

In the example below, we show how to call a data_source with id 519 using the inputs specified above:

const DATA_SOURCE_ID: i64 = 519;
const EXTERNAL_ID: i64 = 0;

fn prepare_impl(input: Input) {
    oei::ask_external_data(EXTERNAL_ID, DATA_SOURCE_ID, input.quote_currency.as_bytes());
}

prepare_entry_point!(prepare_impl);

Note: The external ID for each data source call should be unique

In the previous chapter, we mentioned that the function called prepare() is called. As our function is called prepare_impl, we can use a convenient macro prepare_entry_point, which is a part of owasm-kit, to create a function called prepare() for us.

Execution phase

Now that the request to retrieve the data from the specified data sources has been sent, we can await the results and process it. To get the results, we can call load_input(), which is a part of owasm-kit::ext to get the raw input. Band also provides in owasm-kit the following preprocessing functions for ease of use: load_average(), load_majority(), load_median_integer() and load_median_float().

In the example below, we show how to retrieve and process the data loaded as a median.

fn execute_impl(_input: Input) -> Output {
    Output {
        price: ext::load_median_integer::<u64>(EXTERNAL_ID).unwrap(),
    }
}

execute_entry_point!(prepare_impl);

Similarly to prepare_entry_point, we can use the macro execute_entry_point which is also a part of owasm-kit, to create a function called execute() for us.

Example

Below is an example that shows a complete oracle script that retrieves CoinDesk's Bitcoin Price Index.

use obi::{OBIDecode, OBIEncode, OBISchema};
use owasm_kit::{execute_entry_point, prepare_entry_point, oei, ext};

const DATA_SOURCE_ID: i64 = 519;
const EXTERNAL_ID: i64 = 0;

#[derive(OBIDecode, OBISchema)]
struct Input {
    quote_currency: String,
}

#[derive(OBIEncode, OBISchema)]
struct Output {
    price: u64
}

fn prepare_impl(input: Input) {
    oei::ask_external_data(EXTERNAL_ID, DATA_SOURCE_ID, input.quote_currency.as_bytes());
}

fn execute_impl(_input: Input) -> Output {
    Output {
        price: ext::load_median_integer::<u64>(EXTERNAL_ID).unwrap(),
    }
}

prepare_entry_point!(prepare_impl);
execute_entry_point!(execute_impl);

Schema

Now that the oracle script is defined, we need to define the oracle script's OBI schema as well.

BandChain's OBI schema is structured based similarly to the psuedo-code below:

{input}/{output}

where the following types are supported by obi:

  • Boolean
    • e.g. Input{x: bool} := {x:bool}/{}
  • Unsigned Integers: u8, u16, u32, u64, u128, u256
    • e.g. Input{x: u8} := {x:u8}/{}
  • Signed Integers: i8, i16, i32, i64, i128, i256
    • e.g. Input{x: i8} := {x:i8}/{}
  • Strings
    • Input{x: String} := {x:string}/{}
  • Bytes
    • Input{x: &[u8]} := {x:bytes}/{}
  • Vectors
    • Input{x: vec<u8>} := {x:[u8]}/{}
  • Structs
    • Input{x: vec<u8>, y: String} := {x:[u8],y:string}/{}

For example, using the example code above, the schema would be defined as:

{quote_currency:string}/{price:u64}

Challenge

Here's a challenge to help test your understanding of the knowledge acquired in the past 3 chapters:

Using the following data source ID's: 486, 487 and 488, create an oracle script that queries the given IDs and finds and returns the median result.

The given data source ID contain the following inputs:

FieldTypeDescription
basestrThe base currency of the market to query
quotestrThe quote currency of the market to query
timestampintThe opening timestamp of market candle

and returns the closing price of the specified candle.

The oracle script should take the following input:

FieldTypeDescription
basestrThe base currency of the market to query
quotestrThe quote currency of the market to query
timestampintThe opening timestamp of market candle
multiplierintValue to multiple the price result by

and provide the median prices of the specified candles, where price should be of type u64

An example of the input and output the oracle script should have can be seen below:

#[derive(OBIDecode, OBISchema)]
struct Input {
    base: String,
    quote: String,
    timestamp: u64,
    multiplier: u64,
}

#[derive(OBIEncode, OBISchema)]
struct Output {
    price: u64,
}

An example oracle script that implements these three data sources can be found here and an example request can be seen here and here.

The example inputs and expected outputs for testing can be found below:

Input:

basequotetimestampmultiplier
BTCUSDT1662508800100000000
ETHUSDT1633824000100000000

Output:

price
1929400000000
341422000000

Previous Chapter

Oracle script overview