Chapter 2
Estimated time: 2 hr 26 mins
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
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 }
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:
Parameters | Type | Description |
---|---|---|
external_id | i64 | A unique identifier set within the oracle script which is used to identify a certain request when asking for the results |
data_source_id | i64 | The 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.
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.
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);
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:
Input{x: bool}
:= {x:bool}/{}
u8
, u16
, u32
, u64
, u128
, u256
Input{x: u8}
:= {x:u8}/{}
i8
, i16
, i32
, i64
, i128
, i256
Input{x: i8}
:= {x:i8}/{}
Input{x: String}
:= {x:string}/{}
Input{x: &[u8]}
:= {x:bytes}/{}
Input{x: vec<u8>}
:= {x:[u8]}/{}
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}
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:
Field | Type | Description |
---|---|---|
base | str | The base currency of the market to query |
quote | str | The quote currency of the market to query |
timestamp | int | The opening timestamp of market candle |
and returns the closing price of the specified candle.
The oracle script should take the following input:
Field | Type | Description |
---|---|---|
base | str | The base currency of the market to query |
quote | str | The quote currency of the market to query |
timestamp | int | The opening timestamp of market candle |
multiplier | int | Value 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:
base | quote | timestamp | multiplier |
---|---|---|---|
BTC | USDT | 1662508800 | 100000000 |
ETH | USDT | 1633824000 | 100000000 |
Output:
price |
---|
1929400000000 |
341422000000 |