Polkadot Medianizer
This example demonstrates the functionality of the Laminar chain's oracle pallet including the aggregation of price values.
Laminar Chain
The Laminar chain is built using the Substrate framework. In our example we are going to setup a network consisting if two nodes, one is operated by Alice and one is operated by Bob.
Setup
First we download the Laminar chain sources and bootstrap the network. After that we execute the following statements in the chain's directory.
make init
will setup the environment by downloading and installing all necessary dependencies and the required toolchain.
make build
builds the chain's code so we can execute it.
After that copy the chainSpec.json
file from this repository into the chain's directory. More on the spec file will be explained a little bit later.
Cleanup
When running a node being part of the laminar chain network it creates and stores lots of data because it's constantly building new blocks and extending the actual blockchain. We are going to store all that data in a subdirectory of /tmp
for each node (its base directory). If the base directory doesn't exist it will be created but it is necessary to purge all data of a specific node on subsequent launches:
This wipes the base directory of Alice's node. For wiping Bob's node's data adjust the base-path parameter accordingly. The action asks for confirmation which is given by answering with y
.
Bootstrap the network
We are going to launch Alice's node first:
This creates and runs a node that stores its data in the specified base directory.
We also pass the chain specification file into the node. We use this file to configure the scenario in which all participating parties will own a sufficient amount of gas in order to be able to "pay" for their transmitted transactions.
The parameter
--alice
will make Alice the node's authority uses her predefined key pair.We specify three ports: One for the other peers to connect to the node, a websocket port for the off-chain worker to connect to and a port for incoming RPCs.
After that we fire up Bob's node and tell it to connect to Alice. So in another terminal session we are executing
The main difference is the bootnodes
parameter which specifies the network and identity information of Alice's node.
Use the Laminar chain's oracle
Now the two nodes start working together and our laminar chain network is ready for playing with the oracle.
About oracles
A blockchain oracle is an interface for the chain to the real world and vice versa. By feeding off-chain data to the oracle they enter the chain's scope and can get processed by smart contracts. In this case our oracle is called an inbound oracle. For our example imagine a smart contract that operates on currency prices. It might trigger an action as soon as the price for a specific currency exceeds a specific value. By feeding the current prices from external sources periodically to the oracle we provide the smart contract with the required input data on-chain.
Oracle pallet
A Substrate chain's runtime consists of modules called pallets. In the case of Laminar one of these pallets is our oracle. It can be interacted with in two ways:
Feeding new key-value pairs to the oracle via signed transactions.
Retrieving key-value pairs from the oracle either by other modules on-chain or by off-chain workers via RPC.
The Laminar implementation configures the oracle pallet to use the type CurrencyId
as key type. That means it can be one of the predefined values FBTC, FETH, FEUR, etc. The value uses the type Price
.
In this example we are going to use an off-chain worker to feed currency prices and fetch them afterwards.
Our off-chain worker
The component we create to simulate new price reports and oracle queries lives off-chain and will be written in Typescript. It is completely separate from anything stored on the Laminar chain itself. We rather interact with the chain through specific calls made using the Polkadot API for JavaScript. This API defines the technical aspects of how we can interact with the chain (and specifically with the oracle), i. e. how to build transactions and make RPCs. For the specific parameters regarding the Laminar chain we also make use of the Laminar chain JS SDK.
Let's start by switching the directory
and setting up the dependencies in the package.json
file:
Next step is coding the off-chain worker in the file index.ts
. We import the required types
and build the frame for the program:
Now we can run
for fetching the dependencies and launching the program. However nothing is going on right now so let's write the actual code. From now on we put everything into the async block that we just prepared.
We start creating an API object:
Please note that our API object uses a websocket to communicate with the node. And this websocket connects to port 9945 which we have specified above when firing up Alice's node. So all the requests are submitted to her node.
Next step is configuring the key material. We want to transmit signed transactions that contain the prices to be fed in the oracle. Therefore the off-chain worker needs to know the key pairs of Alice, Bob and Charlie (yes, he's also going to take part in this example). Usually the key pairs would be imported, e. g. from a file. But as for the chain nodes we use the predefined key material which is addressed by //[Name]
. We derive those key pairs from the keyring we create specifying the key pair format SR25519.
Alice wants to trigger a transaction that feeds the oracle. If we start typing api.tx.
the code completion shows us the available extrinsics but the oracle isn't one of them. That's because the default methods of the Substrate runtime are known at compile time but not the custom chain specific extrinsics that might be part of the runtime. Let's find out what's really available by querying the node:
When executing the program we get a list of much more actually available extrinsics - and one of them is laminarOracle
. The list tells us even more about the oracle: the available query method feedValues
. This is exactly what we needed to know.x
Now Alice reports a current BTC price of 1, i. e. she feeds the oracle with the corresponding key-value pair:
This line consists of several interesting pieces:
We use the API object to trigger an extrinsic (
tx
for transaction). The available extrinsics depend on the used chain. In this case we address the oracle pallet of the laminar chain which is addressed bylaminarOracle
.On that extrinsic we call the method
feedValues
in order to submit key-value pairs that should be fed into the oracle. In this case we just submit a single key-value pair for the Bitcoin price.As this operation is a transaction that is going to be included in a block and costs Alice some gas it needs to be signed by her. This is what happens in the last part of the line.
When this line returns the transaction has been finalized and we can now query the oracle with the current Bitcoin price. But there's again the question of how to do that. As mentioned earlier the oracle offers retrieving the current values via RPC. So let's find out the available RPCs:
The output again gives us what we need. Two calls should catch our attention: oracle_getAllValues
and oracle_getValue
. That means we have the two functions getAllValues
and getValue
available for the RPC oracle.
Now we query the current BTC price:
As you can see we use an RPC as the query doesn't change the state of the chain and therefore the operation is free. We specify the correct RPC (which is oracle
) and query the value to the key associated with the Bitcoin price (which is FBTC
). The output is a pair of two values: The current price and the timestamp of the transaction that contained the feeding operation.
That's it! Now we can run the program again with yarn start
. The oracle will be fed and queried and the output is the expected BTC price of 1.
Data aggregation
In the previous section we fed the oracle with one single data point and queried the oracle to retrieve a value. It isn't surprising at all that we read the value that we had written into the oracle just before. But it requires a closer look to what value is actually returned by getValue
. The oracle pallet states that this method returns a "combined value" which indicates that there is some data aggregation happening.
In fact the way the prices of each currency are aggregated is entirely up to the chain implementation. Currently the Laminar chain doesn't implement any aggregation algorithm but the oracle pallet comes with a default implementation: It takes all fed values of a specific currency submitted within the last 3 days and returns the median value. If there aren't any values to calculate the median from it just returns the most recent value.
Let's try it out. Disable the lines of the previous section that interacted with the oracle (via tx and RPC) and add the following:
For more convenience we define a function feedPrice
which takes a price value and a sender and triggers the transaction accordingly. We use the subscription API of Polkadot JS in order to make sure that we wait for every transaction to be finalized in a block. Now our three users submit their values:
When we query the oracle for the BTC price it returns the median value of 5 as expected.
Important! Remember to purge the chain status and restart the two nodes before executing the code of this section.
TL;DR - Quick demo
Download sources for the example and the laminar chain:
Setup laminar chain:
Run first chain node (Answer with "y" when asked for confirmation before purging):
Run second chain node (e.g. in another terminal session):
After both nodes have started to communicate with each other run medianizer example (in yet another terminal session):