Technical Guide: Build a Price Oracle

Chainsight
6 min readJan 10, 2024

--

Using Chainsight to build a simple data relay price oracle

So far, we have built and published several Indexes/Indicators as Showcase by Chainsight Platform. Now we will pick up what has been published in these Showcases and delve deeper into the logic and technical aspects behind them. We hope that these articles will give users and developers a deeper understanding of Chainsight.

First we introduce Price Oracle, which provides a simple data relay. The Price Oracle allows any chain to reference the price of a token or token pair and apply it to trading systems, statistical analysis, etc. One typical configuration pattern in Chainsight’s Project is to relay data collected by a single Snapshot Indexer directly to an EVM-compatible Blockchain by Relayer.

This PriceOracle is built from a single Snapshot Indexer EVM, Algorithm Lens, and Relayer, for a total of three Components.

https://github.com/horizonx-tech/chainsight-showcase/tree/main/price_feed/price_eth

version: v1
label: price_eth
components:
- component_path: components/chainlink_ethusd.yaml
- component_path: components/lens_ethusd.yaml
- component_path: components/relayer_oasyshub_testnet.yaml

Let’s review the roles played by these Components and their implementations. The Manifest described here is extracted only from the parts that affect the actual logic.

Snapshot Indexer EVM

The Snapshot Indexer EVM periodically retrieves data from the Contract’s Function in the specified Blockchain and stores it in the Component. In this PriceOracle component, specify the Price Feed Contract of the Ethereum mainnet’s Chainlink and periodically call a function to retrieve the latest price.

Manifest of Snapshot Indexer for Price Oracle:

metadata:
type: snapshot_indexer_evm
datasource:
location:
id: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
args:
network_id: 1
rpc_url: https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}
method:
identifier: latestAnswer():(uint256)
interface: AggregatorWrapper.json
args: []
interval: ${INTERVAL}

The datasource field is used to set what to call. Which chain is set in datasource.location.args, which contract is set in datasource.location.args, and which function is set in datasource.method. Specifically, datasource.location.id specifies 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 (i.e. Chainlink ETH/USD Price Feed Contract) and datasource.method specifies the latestAnswer():(uint256) of the contract. This Component calls the destination set in datasource with at the intervals set in interval.

Let’s dig deeper into Snapshot Indexer here.

Most of the canister code that expresses the Component of Chainsight is implemented using Rust programming language’s macro, but the following is an extract of the logic for collecting/storing data.

#[ic_cdk::update]
#[candid::candid_method(update)]
async fn index() {
if ic_cdk::caller() != proxy() {
panic!("Not permitted")
}

let current_ts_sec = ic_cdk::api::time() / 1000000;
let res = #contract_struct_ident::new(
Address::from_str(&get_target_addr()).expect("Failed to parse target addr to Address"),
&web3_ctx().expect("Failed to get web3_ctx"),
).#method_ident(#(#request_arg_tokens,)*None).await.expect("Failed to call contract");

let datum = Snapshot {
value: #response_values,
timestamp: current_ts_sec,
};
let _ = add_snapshot(datum.clone());

ic_cdk::println!("timestamp={}, value={:?}", datum.timestamp, datum.value);
}

ref: https://github.com/horizonx-tech/chainsight-sdk/blob/3a2d85d6cab5674a68a03ac180c50984ab4b836d/chainsight-cdk-macros/src/canisters/snapshot_indexer_evm.rs#L204-L224

#contract_struct_ident is a structure that represents Contract using ic-solidity-bindgen, and calls are made to the Function of the Contract specified here. The data acquired and the execution time are included in the structure Snapshot, which is then generated and stored. The storage area is declared as a variable-length array, and acquired data is pushed each time it is acquired. Snapshot Indexer uses stable memory as the data storage area. stable memory is a scalable/migratable memory area in an Internet computer.

The Internet Computer Interface Specification | Internet Computer

Wasm-native stable memory | Internet Computer

Algorithm Lens

Algorithm Lens can perform arbitrary calculations based on specified data sources. Specify one or more Chainsight Platform Components as data sources. Template code is generated to make that data source available to the calculation logic, and the user writes any logic on top of this.

Manifest of Algorithm Lens for Price Oracle:

metadata:
type: algorithm_lens
datasource:
methods:
- id: chainlink_ethusd
identifier: 'get_last_snapshot_value : () -> (text)'
candid_file_path: src/canisters/chainlink_ethusd/chainlink_ethusd.did
func_name_alias: chainlink_ethusd

The template code for Algorithm Lens contains code to call other Components in the Chainsight Platform as data sources for the calculation. This data source is specified in the datasource field. One element of datasource.methods is defined for one type of data source. datasource.methods[i]. {identifier,candid_file_path} to specify the Interface of the calling Component, so that Algorithm Lens can handle the interface of the calling datasource. This generates the template code. Here is an example of a template code.

use sample_lens_accessors::*;
#[derive(Clone, Debug, Default, candid :: CandidType, serde :: Deserialize, serde :: Serialize)]
pub struct LensValue {
pub dummy: u64,
}
pub async fn calculate(targets: Vec<String>) -> LensValue {
let _result = get_chainlink_ethusd(targets.get(0usize).unwrap().clone()).await;
todo!()
}

A function that calls the data source specified in Manifest earlier is generated and used in the following line.

let _result = get_chainlink_ethusd(targets.get(0usize).unwrap().clone()).await;

While using this, we will code our own logic into the Algorithm Lens.

By the way, what is the logic required by Price Oracle’s Algorithm Lens? The goal is to have Relayer propagate pricing information to the EVM. This time, this price will be scaled to integers so that it can be handled by EVM. If the propagation source values are obtained from Snapshot Indexer EVM, no special processing is required, but if Snapshot Indexer HTTPS or Snapshot Indexer ICP is used, the actual calculated source data may be a fraction.

The final code will be as follows

use sample_lens_accessors::*;
const PRECISION: u32 = 18;
pub type LensValue = u128;
pub async fn calculate(targets: Vec<String>) -> LensValue {
let ethusd = get_chainlink_ethusd(targets.get(0usize).unwrap().clone())
.await
.unwrap()
.parse::<u128>()
.unwrap();
format_ethusd(ethusd)
}

// The raw data is 8 digits precision, so we need to convert it into 18 digits
fn format_ethusd(ethusd: u128) -> u128 {
ethusd * 10u128.pow(PRECISION - 8)
}

#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn test_format_ethusd() {
let ethusd = 2956575400000;
let formated = format_ethusd(ethusd);
assert_eq!(formated, 29565754000000000000000);
}
}

Since it can be written in the Rust programming language, you can write a Unit Test for your logic and guarantee its quality.

Relayer

Relayer can propagate a given data source to a Contract in a given EVM-compatible Blockchain. The Function to propagate to the Contract is also included in the configuration, and the Transaction must be generated and signed before it can be written to an EVM-compatible Blockchain Contract. Integrating Internet Computer’s Threshold ECDSA, the Chainsight Relayer Component enables integration with EVM.

Threshold ECDSA: chain-key signatures | Internet Computer

Manifest of Relayer for Price Oracle:

metadata:
type: relayer
datasource:
location:
id: lens_ethusd
method:
identifier: "get_result : (vec text) -> (nat)"
interface: null
args: []
lens_targets:
identifiers:
# id of lens_wbtcusd
- chainlink_ethusd
destination:
network_id: 9372 # Oasys Hub Testnet
type: uint256
oracle_address: ${ORACLE_ADDRESS}
rpc_url: https://rpc.testnet.oasys.games/
interval: ${INTERVAL}

As in the past, the datasource field is used to set the data source. The destination field defines the write destination. destination.{network_id,oracle_address,rpc_url} to uniquely identify the destination Blockchain, Contract. destination.type specifies what type to write as. Chainsight also deploys Oracle, to which it writes, and has published its code. This Oracle allows writing arbitrary Scalar values and allows storing values per Relayer.

horizonx-tech/chainsight-management-oracle

With only three Components, you can build an arbitrary Price Oracle. You can create your own Price Oracle simply by updating it with your own data source!

Stay connected with us for more exciting updates and explore how YOU can build the future of DeFi!

Together, let’s continue to shape a more modular and innovative financial future!

All the best,

The Chainsight Team 🚀😎

--

--

Chainsight

On-chain Data Extension Layer for all blockchains. The interchain layer to synchronize historical data, process it, and bring it for any blockchain.