Getting started tutorial
Project overview
In this tutorial we are creating a DApp on Arbitrum Sepolia, which consumes the data from Binance public API. To see the basic capabilities, we are also going to use the post-processing of the data on the Quex Data Oracle Side making a simple script (or filter in jq
terms) for this purpose, and illustrate the non-trivial return structure.
Suppose the DApp collects the order books for BTC/USDT pair for its logic. It needs the following data from Binance:
The sequential number of the update to keep track of the ordering (Binance returns it as
lastUpdateId
)Five best bids
Five best asks
Both bid and ask are required to be tuples of integer numbers (price, quantity). The precision is required to be 8th digit after decimal point. That is, the prices are to be multiplied by 100,000,000 and returned as uint256
.
Design the data structures
In line with the problem statement, our contract will work with the following data structures:
Deploy receiving contract
Here is an example of a simple contract keeping track of the last created request and storing the response data.
As we are passing the arrays of nested structures, we need to compile the contract with the intermediate representation. For example, if you are using Remix IDE, go to compiler settings, enable Use Configuration File
in the Advanced configurations
, and add "viaIR": true
to your compiler_config.json
:
Register action
According to the Quex architecture, the two things need to be done for data to be shipped. First, get the Action Id from the oracle pool. In our case, the pool is the Quex Request Pool. The action must consist in performing HTTPS request to Binance open API. Since this action is quite specific, the pool does not know it in advance. So we need to register this action on the pool contract and get its id. If you are interested in the specifics of this process, consult the Request Pool Description. In this tutorial we use the helper tool to create both action and flow simultaneously, so let us go through the idea behind the flow creation first.
Create flow
After the action id is known, it is time to define the route of data delivery. That is, to tell Quex Core what oracle pool is the data supplier, what action is expected to be performed by it for the particular demand, what is the address of the data consumer (including callback selector), and what gas consumption to expect from the callback (for relayer reimbursement). As a result, we will get flow id which can be used for making requests and pushing the data without passing the specific details every time. For the structures involved, please consult Flow Creation.
To save time on these contract interactions, we will use the Flow Creation Tool. So, let us pass to this part
Use flow creation script
Configure settings
First, edit config.json
. Compare the addresses of oracle pool and Quex Core with the ones you can find here. Verify that rpc_url
indeed points to Arbitrum Sepolia RPC. We can see from Remix IDE that gas limit of 700k should be more than enough for processResponse
call. The value of td_pubkey
can be found either in our Core Contract (see ITrustDomainRegistry
, REPORT_DATA
field of the TD Quote), or on addresses page. In case your request will not have private data, the td_pubkey
does not matter.
Make sure that consumer
points to your contract, and the callback selector points to your method. If you use Remix IDE, you can find the selectors in Solidity Compiler->Compilation Details->Function Hashes
.
Configure request
Now, we edit request.json
. It defines the structure that will be passed to addAction
call. The request
field has the general structure of HTTP request that will be performed by the oracle. However, there is also similarly looking patch
field. This field contains the private data which will be added to the request inside the TD. The TD accepts it in encrypted form. Why is it in plaintext here then? The flow creation script encrypts it prior to sending using the td_pubkey
previously specified in the config. The pathSuffix
is concatenated to the path in the request
, the headers and parameters are added to those of request
, the body in patch
. In case the body in patch
is non-empty, it will replace the body
from the request
.
The query we are tailoring accesses the host www.binance.com
, path /api/v3/depth
, has header Content-Type: application/json
, and parameters limit=5
and symbol=BTCUSDT
. If one tries this query, the response from Binance API is like
Now, we need to let the oracle know how to convert this JSON file to solidity structs used by our contract. To do so, first define responseSchema
as Solidity ABI schema for the OrderBook
structure. Namely, (uint256,(uint256,uint256)[5],(uint256,uint256)[5])
. Now we need to instruct the oracle to post-process response in a mixed-type array which can be cast to this type. Quex Request Oracle Pool uses a subset of jq language for JSON post-processing. Jq programs are also called filters. We start building the filter step by step.
To cast a number from string to desired format,
tonumber*100000000 | floor
can be used.Now, the filter
.bids[0] | map(tonumber*100000000 | floor)
would yield the first bid converted to the right formatTo convert all the bids to the necessary format, apply this map as nested:
.bids | map(map(tonumber*100000000 | floor))
To reuse this filter for asks, process bids and asks as an array:
[.bids, .asks] | map(map(map(tonumber*100000000 | floor)))
. Now we have two arrays adhering to the encoding.Finally, prepend it with the value of
lastUpdateId
:[.lastUpdateId] + ([.bids, .asks] | map(map(map(tonumber*100000000|floor))))
Combining it all together, the request.json
file may now look as follows:
Note that we did not include any patch as we do not need the private data in this particular case.
Run the flow creation script
In order to initiate the transactions, the script needs access to the secret key. It is easiest to pass it as an environment variable. However, you can also add it to .env
or to config.json
(see readme for the script)
The script will output the id of registered action; flow id, the fee per request in native coins, and the amount of gas to be covered per request.
Estimate fee
In our case, the tool has already shown the fee values. However, if we needed to access them from other project, we could use getRequestFee(uint256 flowId)
method of the Quex Core which returns this tuple. The value which must be attached to the transaction is nativeFee + gasPrice*gas
. Suppose, the call returned 30000000000000
Wei as nativeFee
and 810000
as gas. Suppose also that gas price is 0.1 GWei That means, the request creating transaction must have at least 110000
GWei in value. It is safe to round this value up, as Quex Core returns the change.
Send request
Once the value is estimated, the request can be created by calling request
function on our contract with the value taken from the first step. This transaction submits the on-chain request that is captured by the pool relayer, and transferred to the oracle in the pool. After the oracle completes the task, the post-processed data are relayed to Quex Core contract. Quex Core verifies the authority of the signing Trust Domain for this particular action, checks the signature, and sends the data to our callback.
Check the result
Check out your view functions to see that order books are indeed delivered.
Last updated