Harbinger Wrapper

Harbinger is an on-chain oracle that provides a signed price feed for digital assets. The feed represents normalized price candles with the following data: (1) start time, (2) end time, (3) open price, (4) high price, (5) low price, (6) close price, and (7) volume as well as an entrypoint for smart contracts to query the volume weighted average price for an asset over the past 6 updates.

Price data is independently updated with signed calls from different centralized exchanges (Coinbase, Binance, Gemini, OKEx). A separate Harbinger oracle is deployed for each price feed source. Currently, the only exchange that regularly posts price data is Coinbase.

Technically, a Harbinger oracle consists of two smart contracts, a storage contract that stores latest price updates and a normalizer contract that computes a volume-weighted average price from the last n updates.

Queries

Import Plugins

#import { Query, Connection } into Tezos from "w3://ens/tezos.web3api.eth"

Response Types

Get Asset Response

Type response for getting asset last candle details.

type GetAssetResponse {
  low: String!
  open: String!
  high: String!
  asset: String!
  close: String!
  volume: String!
  endPeriod: String!
  startPeriod: String!
}

List Providers Response

type listProvidersResponse {
  providers: String
}

List Assets Response

type listAssetsResponse{
  assets: String
}

Get Candle Response

type GetCandleResponse{
  low: String!
  open: String!
  high: String!
  asset: String!
  close: String!
  volume: String!
  endPeriod: String!
  startPeriod: String!
}

Get Normalized Price Response

type GetNormalizedPriceResponse {
  price: String!
}

Network

Select between various Tezos networks.

enum Network {
  custom
  mainnet
  ghostnet
  jakartanet
}

Custom Connection

Define custom connection parameters.

type CustomConnection {
  connection: Tezos_Connection!
  oracleContractAddress: String!
}

Query Functions

Get Asset Data

Returns data of a particular asset when the asset’s code is passed.

getAssetData(
    network: Network!
    assetCode: String!
    custom: CustomConnection
  ): GetAssetResponse!

List Assets

List available price pairs for the requested provider. This can be achieved by reading the storage of the normalizer contract for provider and extracting the list of strings from storage.assetCodes.

 listAssets(
    network: Network!
    custom: CustomConnection
    providerAddress: String!
  ): listAssetsResponse!

Get Candle

Reads the most recent price data (OHLCV plus timestamps) sent by the provider. This can be achieved by reading the storage of the storage contract for provider and extracting the contents of bigmap oracleData at key assetCode.

 getCandle(
    network: Network!
    custom: CustomConnection
    assetCode: String!
    providerAddress: String!
  ): GetCandleResponse!

List Providers

List price providers, returns a list of unique provider identifiers (names) that can be used in subsequent functions to query the relevant contracts.

 listProviders: listProvidersResponse!

Get Normalized Price

Reads the normalized price for asset assetCode from oracle feed for provider.

 getNormalizedPrice(
    network: Network!
    custom: CustomConnection
    assetCode: String!
    providerAddress: String!
  ): GetNormalizedPriceResponse!

Oracle Data

All price oracle data is represented in two formats:

  • A standard unix timestamp for candle starts and ends

  • A natural number for prices and volumes, with six digits of precision. For instance, the price $123.45 would be represented as 123450000.

Creating a Web3 client with Tezos Plugin support

import { Web3ApiClient } from "@web3api/client-js"
import { tezosPlugin } from "@web3api/tezos-plugin-js"
export const client = new Web3ApiClient({
    plugins: [{
        uri: "w3://ens/tezos.web3api.eth",
        plugin: tezosPlugin({
            networks: {
                mainnet: {
                    provider: "https://rpc.tzstats.com"
                },
                granadanet: {
                    provider: "https://rpc.granada.tzstats.com",
                }
            },
            defaultNetwork: "mainnet"
        })
    }]
})

Call a function from the URI via Polywrap + GraphQL

import { client } from './client'
const HARBINGER_URI = 'w3://ipfs/QmYEz2Zxr5Zd3UuWs2ijocsGkvaoLurkzXJb1Wc9NK1nWt'
export const getAssetData = async (assetCode, network) => {
    return await client.query({
        uri: HARBINGER_URI,
        query: 
        `query {
		getAssetData(
		network: $network,
		assetCode: $assetCode,
		)}`,
        variables: {
            assetCode,
            network
        }
    });
}

Wrapper Example

The Harbinger wrapper can be found in the ./tezos/harbinger/wrapper folder. Install the node packages and build plugin-js

yarn
yarn build

Running Tests

The e2e tests can be found in the src/tests/e2e folder. Run the e2e tests as follows:

yarn test

Project Structure

Queries

Can be found in the ./src/query folder containing the index.ts file which is the AssemblyScript query logic and schema.graphql file which contains the GraphQL schemas for the functions in the query's index file.

export function getAssetData(input: Input_getAssetData): GetAssetResponse {
    if (input.network == Network.custom && input.custom === null) {
        throw new Error(`custom network should have a valid connection and oracle contract address `)
    }
    let oracleContractAddress: string = "KT1Jr5t9UvGiqkvvsuUbPJHaYx24NzdUwNW9";
    let connection: Tezos_Connection = {
        provider: "https://rpc.tzstats.com",
        networkNameOrChainId: "mainnet"
    };
    switch (input.network) {
        case Network.granadanet:
            connection = {
                provider: "https://rpc.granada.tzstats.com",
                networkNameOrChainId: "granadanet"
            }
            oracleContractAddress = "KT1ENR6CK7cBWCtZt1G3PovwTw3FgSW472mS";
            break;
        case Network.custom:
            connection = input.custom!.connection;
            oracleContractAddress = input.custom!.oracleContractAddress;
            break;
    }
    const storageValue = Tezos_Query.getContractStorage({
        address: oracleContractAddress,
        connection: connection,
        key: "oracleData",
        field: input.assetCode
    });
    const assetData = JSON.parse(storageValue);
    return {
        low: normalizeValue(parseFloat(getString(assetData, "4"))),
        open: normalizeValue(parseFloat(getString(assetData, "2"))),
        high: normalizeValue(parseFloat(getString(assetData, "3"))),
        asset: input.assetCode,
        close: normalizeValue(parseFloat(getString(assetData, "5"))),
        volume: normalizeValue(parseFloat(getString(assetData, "6"))),
        endPeriod: getString(assetData, "1"),
        startPeriod: getString(assetData, "0"),
    };
}

Tests

To test the functions in query/index.ts, e2e tests are written in the __tests__/e2e folder.

it("should get asset data for `XTZ-USD` on mainnet", async () => {
    const response = await client.query < {
        getAssetData: QuerySchema.GetAssetResponse
    } > ({
        uri: ensUri,
        query: 
        `query {
	    getAssetData(
	    assetCode: $assetCode,
	    network: mainnet)}`,
        variables: {
            assetCode: "XTZ-USD",
            network: "granadanet",
        }
    })

    expect(response.errors).toBeUndefined()
    expect(response.data).toBeDefined()
    expect(response.data?.getAssetData).toBeDefined()
    expect(response.data?.getAssetData.low).toBeDefined()
    expect(response.data?.getAssetData.open).toBeDefined()
    expect(response.data?.getAssetData.high).toBeDefined()
    expect(response.data?.getAssetData.asset).toBeDefined()
    expect(response.data?.getAssetData.close).toBeDefined()
    expect(response.data?.getAssetData.volume).toBeDefined()
    expect(response.data?.getAssetData.endPeriod).toBeDefined()
    expect(response.data?.getAssetData.startPeriod).toBeDefined()
})

Last updated