Skip to main content

Writing An App

info

This app is based on AssemblyScript.

Writing an App

Each app on the Steer Protocol utilizes an on-chain smart contract. The goal of an app is to write logic which can interact with the given contract. For this guide we are going to focus on developing an app which can be used with the UniLiquidityManager on Steer Protocol.

The Goal

Create an app which uses a liquidity position to provide a trailing range order for a given asset pair. For more information on range orders please see the Range Order documentation by Uniswap.

Getting Started

To get started we will use the Assembly script template repository which is available on Github. This repository contains a template which can be used to build a concentrated liquidity app, in addition to more general apps as well.

tip

The AssemblyScript template can be found here: https://github.com/SteerProtocol/strategy-template-assemblyscript

Template cloning

Project Structure

Apps have three external functions that are used by the Steer system. Additional methods, classes, or varaibles can be used in conjunction with these required functions for any desired behavior. For more information please see the app interface. This design means, that as a developer, you only need to implement the methods which are required for the app to work.

Below are the significant files and folders which you will want to get familiar with:

├── assembly      // Source code for the app
├── build // Output of the build process aka `yarn asbuild`
├── coverage // Coverage report for testing
├── tests // Test files with a built in test runner
├── asconfig.json // Assemblyscript config
├── index.js // Javascript entrypoint for the app when running tests
├── package.json // Dependencies for the app

Project Setup

Once the template has been cloned, you will need to install the project dependencies. This can be done via the following command:

  yarn install
info

You will notice that there is a post-install script which will compile the ./assembly source folder and populate the ./build folder. This is done to make it easier to run the tests. We will cover this later.

Once you have set up your project, you can begin defining your app.

Setup App Params

Almost all apps will require parameters to be passed into the app. These parameters are defined via the config method of the app. The config should return proper JSON Schema which can be used to determine and validate the parameters.

info

If you are not familiar with JSON Schema the following guide is very helpful: JSON Schema Guide

For our goals, we need to require users to provide a percentage which they would like to tail the current price of an asset pair. Let's accomplish this by adding a percentage parameter to the config. We will also specify that the input is a required number. We will also add a binWidth parameter which will be with size in ticks of the position to make. This number needs to be a multiple of the pool tick spacing to be valid, which we can check and use for more precise bins by requiring poolFee as well. The final result of the config function should be a valid JSON Schema string.

tip

You can find an online JSON Schema builder with examples here: Online JSON Schema Builder

// This is only called when the bundle is being newly implemented, and the resulting configuration object is stored in IPFS.
export function config(): string{
return `{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Strategy Config",
"type": "object",
"expectedDataTypes": ["OHLC"],
"properties": {
"percent": {
"type": "number",
"description": "Percent for trailing stop order",
"default": 5.0
},
"poolFee": {
"description": "Pool fee percent for desired Uniswapv3 pool",
"enum" : [10000, 3000, 500, 100],
"enumNames": ["1%", "0.3%", "0.05%", "0.01%"]
},
"binWidth": {
"type": "number",
"description": "Width for liquidity position, must be a multiple of pool tick spacing",
"default": 600
}
},
"required": ["percent", "binWidth", "poolFee"]
}`;
}

info

Inside the static config, there is a extra field called parameters which define the type of data used by the app.

Once you have defined your config, you must write the initialize function. A fresh instance of the bundle is ran each epoch, thus no information persists. The initialize function allows us to reconfigure the bundle. The function should parse and store any variables required for the execute function. In our case we will fetch the percentage, binWidth, and poolFee variables.


import { JSON } from "assemblyscript-json";

// Variables stored in the wasm memory
let width: i32 = 600;
let percent: f32 = 0;
let poolFee: i32 = 0;

export function initialize(config: string): void {
// Parse the config object
const configJson = <JSON.Obj>JSON.parse(config);
// Get our config variables
const _width = configJson.getInteger("binWidth");
const _poolFee = configJson.getInteger("poolFee");
const _percent = configJson.getValue("percent");
// Handle null case
if (_width == null || _percent == null || _poolFee == null) {
throw new Error("Invalid configuration");
}

// Handle percents presented as integers
if (_percent.isFloat) {
const f_percent = <JSON.Num>_percent
percent = f32(f_percent._num);
}
if (_percent.isInteger) {
const i_percent = <JSON.Integer>_percent
percent = f32(i_percent._num);
}
// Assign values to memory
width = i32(_width._num);
poolFee = i32(_poolFee._num);
}

Logic Implementation

Now that we have proper input controls, the execute function can be written. There are some pre-made utility functions available from the @steerprotocol/strategy-utils module. For our goals we will import the following class and functions:

import {Position, parsePrices, getTickFromPrice, trailingStop, renderULMResult, getTickSpacing} from "@steerprotocol/strategy-utils";

This will allow us to:

  1. Make position objects
  2. Parse OHLC data from the data connector
  3. Convert prices into ticks
  4. Calculate our trailing stop price based off our trailing stop percentage
  5. Convert an array of positions into a format digestable by the node and the UniLiquidityManager
  6. Get the minimum tick spacing based on the pool fee tier

With these methods available, we first parse the prices object. This method assumes the first data connector returns OHLC data, (it will need editing if the order is different) from which we can use the trailingStop method to get our stop loss price. At this point we need to add some custom logic, so we make a helper function called calculateBin to make our position array. Here it helps to understand some of the inner workings of Uniswap's concentrated liqudity, take some time to understand the concepts at play (especially ticks in this example). We start by converting our trailing stop price to a tick, where we then find the nearest viable ticks to make the position at. We add our position to an array to be passed into the renderULMResult method. This method properly formats the position(s) into a stringified object that will be returned by the execute function.


export function execute(_prices: string): string {

// _prices will be a nested array with the OHLC data in the deeper array (at index 0)
// We pass the whole array in and specify which position has the candles
const prices = parsePrices(_prices, 0);

// If we have no candles
if (prices.length == 0) {return `skip tend, no candles`}

// Get Trailing stop price
const trailingLimit = trailingStop(percent, prices)

// Calculate position
const positions = calculateBin(trailingLimit);

// Format and return result
return renderULMResult(positions);
}


function calculateBin(upper: f32): Position[] {

// Calculate the upper tick based on the start of the stop
const upperTick: i32 = i32(Math.round(getTickFromPrice(upper)));

// Get the spacing
const tickSpacing = getTickSpacing(poolFee);

// Step down ticks until we reach an initializable tick
let _startTick: i32 = upperTick;
while (_startTick % tickSpacing !== 0) {
_startTick--;
}

const positions: Array<Position> = [];
const position = new Position(_startTick - width, _startTick, 1);
positions.push(position);
// Return the position in an array to use with the renderULMResult method
return positions
}

Testing

The template comes with two app logic tests. The first test is simple; it will test the logic of the config method. The second test is more complex and will test the logic of the execute method.

Let's make sure the test can run by using the following command:

  yarn test

You should see the following output:

Successful tests

Final Result

As you can see it can be very easy to create a simple app for the Steer Protocol! It doesn't take long to build more tools to develop complex apps. Using wasm allows network speeds to be lighting fast. The final payload from the execute method will be hashed and signed by the node operators and subsequently by the keepers. The final execution is done through the Orchestrator.

Next Steps

tip

Use the Steer Protocol Backtesting Platform to test your App!

Steer provides a state-of-the-art backtesting platform for all concentrated liquidity apps! Run your execution bundle on historical data and see how it would have performed! Examine risks and debug your app to improve its performance. You can find more information about this tool in the Steer Backtesting Documentation