Required Interface
The Steer Protocol is possible due to a robust and flexible API which allows developers to build their own data connectors. Below is the interface for the v1.0.0 of the Steer Protocol.
This interface must be adhered to in order to be compatible with the Steer Protocol.
Data connectors consist of 4 main functions.
Configuration Form
Most data connectors and transformations will require configuration. JSON Schema is used to provide the proper interface between bundles and their consumers (keeper and interfaces). The configured JSON Schema is also used to validate configuration of the data connector params on the Steer Protocol when being set. The configuration object defined by the schema definition is passed into the initializer of the data connector.
After the bundle is uploaded to IPFS, anyone can use the same bundle for another purpose. This function allows for the required configuration parameters to be set upon implementation and is unique to the requirements of each use-case.
Check out https://rjsf-team.github.io/react-jsonschema-form/ for quick development.
Interface
export function configForm(): string
Example (Assemblyscript)
export function configForm(): string {
return `{
"title": "Uniswapv3 Swap To Candle Config",
"description": "Input config for converting swap data from a Uniswap v3 pool into OHLC data",
"type": "object",
"required": [
"candleWidth",
"poolAddress",
"period"
],
"properties": {
"poolAddress": {
"type": "string",
"title": "Pool Address",
"description": "Address of the pool to pull swaps from"
},
"period": {
"type": "integer",
"title": "Period",
"description": "Duration in seconds of how far back in time from the current to pull swap data for"
},
"candleWidth": {
"type": "integer",
"title": "Candle Width",
"description": "The size or width of each candle to make from the swap data, measured in seconds"
}
}
}`;
}
Initialize
When the bundle is first pulled by the Keeper node for execution, the associated configuration file created from the input form above will be passed into this function. The second parameter is the timestamp that all nodes will use to sync any time-dependant calculations to maintain determinism.
Interface
export function initialize(config: string, timestamp: i32): void
Example (Assemblyscript)
export function initialize(config: string, _timestamp: i32): void {
// Parse through the config and assign local variables...
const configObj = <JSON.Obj>JSON.parse(config);
const _poolAddress = configObj.getString("poolAddress");
const _period = configObj.getInteger("period");
const _candleWidth = configObj.getInteger("candleWidth");
// Ensure no variables are null
if (_poolAddress == null || _period == null || _candleWidth == null) {
throw new Error("Flawed config");
}
// Assign
poolAddress = _poolAddress._str;
timestamp = _timestamp;
period = i64(_period._num);
startTime = timestamp - period;
candleWidth = i64(_candleWidth._num);
// Any other initial logic...
}
Main
To support asyncronous functionality with the WASM bundle, and to ensure that any custom validation logic can be implemented, bundles must use the main function as a deferred callback function. The Keeper node will call this function until the string "true" is returned. On the first iteration, the string "first" will be passed into the main function as the response parameter. All data returned by this function (except "true" which signals no more requests are needed) should be a stringified Axios Request Config Object. The Keeper node will send this request and pass the response back into the main function.
Interface
export function main(response: string): string
Example (Assemblyscript)
In this example, we are collecting swap data from the Uniswapv3 subgraph. We are limited to fetching 1000 swaps per call so multiple calls may be necessary, depending on the period, to fetch all the data. After each call we request more data that is past the latest swap we have, until there are no more swaps to be retrieved. Then "true" can be returned to exit the callback loop.
export function main(response: string): string {
// Presumably the first call
// first is defined as a const in memory
if (response == first) {
return `{
"method": "post",
"url": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
"headers": {},
"data": {
"query": "{ swaps (first: 1000, skip: 0, where: {timestamp_gt: `+ startTime.toString()+`, timestamp_lt: `+timestamp.toString()+`, pool: \\"`+poolAddress+`\\"}, orderBy: timestamp, orderDirection: asc){id, timestamp, amount0, amount1, transaction {id, blockNumber}, tick, sqrtPriceX96}}"
}
}`
}
else{
// We have a response, parse it and test condition, update iteration logic
const new_response = <JSON.Obj>JSON.parse(response);
const new_data = new_response.getObj("data");
if (new_data == null) {throw new Error("No data in response");}
const new_arr = new_data.getArr("swaps");
if (new_arr == null) {throw new Error("No swaps in response");}
const new_swaps = new_arr._arr;
// If we recieved 0 swaps, we have exhausted the remaining data, so we have completed our loop
if (new_swaps.length == 0) {
// end loop
return "true";
}
else {
// update data
data = data.concat(new_swaps);
const last_swap = <JSON.Obj>new_swaps[new_swaps.length - 1]
const _timestamp = last_swap.getString("timestamp");
if (_timestamp == null) {throw new Error("No timestamp in last swap");}
// update iteration logic, TheGraph has a skip limit of 5, so we use the start date to filter
startTime = i32(parseInt(_timestamp._str));
return `{
"method": "post",
"url": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
"headers": {},
"data": {
"query": "{ swaps (first: 1000, skip: 0, where: {timestamp_gt: `+ startTime.toString()+`, timestamp_lt: `+timestamp.toString()+`, pool: \\"`+poolAddress+`\\"}, orderBy: timestamp, orderDirection: asc){id, timestamp, amount0, amount1, transaction {id, blockNumber}, tick, sqrtPriceX96}}"
}
}`
}
}
}
Transform
The transform function is the last code run in the bundle, and returns the shaped data to be consumed elsewhere. Here, logic for how the collected data can be mutated will be executed.
Interface
export function transform(): string
In this example, the swap data is transformed into OHLC data and returned as an object to be consumed by an app. The result of your data connector must be an object with the element data. This will be striped when adding all data connector results into an array to pass into the execution bundle.
Example (Assemblyscript)
export function transform(): string {
const X96 = Math.pow(2,96);
// Gen arr of prices, data is already ordered by timestamp
const prices: Array<f32> = [];
const timestamps: Array<i32> = [];
// Fill our arrays with properly typed data
for (let i = 0; i < data.length; i++) {
const swap = <JSON.Obj>data[i];
const _sqrtPriceX96 = swap.getString("sqrtPriceX96");
const _timestamp = swap.getString("timestamp");
if (_sqrtPriceX96 == null || _timestamp == null) {continue;}
const _price = f32(parseFloat(_sqrtPriceX96._str));
const _timestamp_i32 = i32(parseInt(_timestamp._str));
prices.push(f32(_price/X96));
timestamps.push(_timestamp_i32);
}
// Get start point, and interval
let candle_index = timestamp - period;
const Candles: Array<Candle> = [];
// Loop through all intervals, and create candles
while (candle_index < timestamp) {
// Get batch of prices for this interval
const prices_batch: Array<f32> = [];
for (let i = 0; i < timestamps.length; i++) {
if (timestamps[i] >= candle_index && timestamps[i] < candle_index + candleWidth) {
prices_batch.push(prices[i]);
}
}
//Now we have our batch of prices, calculate & push OHLC
if (prices_batch.length != 0) {
Candles.push(getCandle(prices_batch));
}
// Increment candle_index
candle_index += candleWidth;
}
// craft object to return
return "{data: [" + Candles.toString() + "]}";
}
Not included in the example code are the candle types and helper functions.
Version
Steer is ever evolving, and our interfaces might change. We will offer backwards compatibility for all versions we introduce.
Interface
export function version(): i32
As more versions are released the breaking changes will be listed here.
Example (Assemblyscript)
export function version(): i32 {
return 1;
}