Required Interface
Steer Protocol's app engine has a robust and flexible API that allows developers to build their own data connectors. Below is the interface for v2.0.0 Data Connectors.
This interface must be adhered to in order to be compatible with the Steer Protocol.
Data connectors consist of 4 main exported functions written in any WebAssembly-compatible language.
In this example, we use the AssemblyScript language.
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 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/ to quickly create JSON Schemas
Imports
import { JSON } from "json-as/assembly"
import { fetchSync } from "as-fetch/sync"
Interface
export function config(): string
Example (Assemblyscript)
export function config(): 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 and some execution context. The execution context object contains various information that might be needed when making a time deterministic API call. This context object is appended to the configuration object passed into the initialize funtion.
Exection Context
export class ExecutionContext {
executionTimestamp: number = 0
epochLength: number = 0
epochTimestamp: i32 = 0
vaultAddress: string = ""
blockTime: i32 = 0
blockNumber: i32 = 0
}
Interface
export function initialize(config: string): void
Example (Assemblyscript)
@serializable
class Config {
candleWidth: string = ""
poolAddress: string = ""
subgraphEndpoint: string = ""
epochTimestamp: u64 = 0
isValid(): boolean {
return
!!this.candleWidth &&
!!this.poolAddress &&
!!this.subgraphEndpoint &&
!!this.epochTimestamp
}
}
let configObj: Config = new Config()
export function initialize(config: string): void {
configObj = JSON.parse<Config>(config)
if (!configObj.isValid()) {
throw new Error("Config not properly formatted")
}
}
Execute
To support asyncronous functionality with the WASM bundle, and to ensure that any custom validation logic can be implemented, bundles must use the execute function as a deferred callback function. The Keeper node will call this function until the string "true" is returned. On the first iteration, an empty string will be passed into the execute 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 exectue function.
Interface
export function execute(): void
Example (Assemblyscript)
In this example, we are collecting swap data from the Uniswapv3 subgraph. We are limited to fetching 1000 swaps per call on the graph, so multiple calls may be necessary depending on how long our period is. Here we will to fetch all the data (limited to 500 to help with rate limiting). 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. Keep in mind that subgraph calls are chain specific, this data connector will fetch data from Ethereum mainnet.
export function execute(): void {
if (!configObj) throw new Error("Missing config: Must call config() first!");
while (true) {
const res = fetchSync(configObj.subgraphEndpoint, {
method: "POST",
mode: "no-cors",
headers: [
["Content-Type", "application/json"]
],
body: String.UTF8.encode(
`{"query":"{ swaps (first: 500, skip: 0, where: {timestamp_gt: ${currentTimestamp}, timestamp_lt: ${configObj.epochTimestamp}, pool: \\"${configObj.poolAddress.toLowerCase()}\\"}, orderBy: timestamp, orderDirection: asc){id, timestamp, amount0, amount1, transaction {id, blockNumber}, tick, sqrtPriceX96}}"}`
)
});
const swapText = res.text();
if (!res.ok) break;
if (swapText.length <= '{"data":{"swaps":[]}}'.length) break;
new SwapParser(swapText).parseTo<Swap>(swaps);
const lastSwap = swaps[swaps.length - 1]
currentTimestamp = i64.parse(lastSwap.timestamp);
}
}
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 OHLCV 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 X192 = Math.pow(2, 192);
const rawTradeData: Array<RawTradeData> = [];
for (let i = 0; i < swaps.length; i++) {
const swap = swaps[i];
const sqrtPriceX96 = f64.parse(swap.sqrtPriceX96)
rawTradeData.push(new RawTradeData(i32.parse(swap.timestamp), (sqrtPriceX96 * sqrtPriceX96) / X192, f64.parse(swap.amount0)))
}
const formattedCandles = generateCandles(JSON.stringify<Array<RawTradeData>>(rawTradeData), configObj.candleWidth);
return formattedCandles;
}
Not included in the example code are the candle types and helper functions. There are a few basic JS libraries imported into the WebAssembly loader during runtime. In our example we use pond.js to make the candle data.
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;
}