Comment on page
Request-Response
Arbitrary Off-Chain Data Available To Your Smart Contract
A detailed example of how to use the Orakl Network Request-Response can be found at example repository
request-response-consumer
.The Orakl Network Request-Response serves as a solution to cover a wide range of use cases. While it may not be possible to bring every data feed directly to the blockchain, the Request-Response allows users to specify within their smart contracts the specific data they require and how they should be processed before they are received on-chain. This feature returns data in Single Word Response format, providing users with greater flexibility and control over their data, and allowing them to access a wide range of external data sources.
Orakl Network Request-Response can be used with two different account types that support prepayment method:
Permanent Account allows consumers to prepay for Request-Response services, and then use those funds when interacting with Orakl Network. Permanent account is currently a recommended way to request for Request-Response. You can learn more about prepayment payment method or permanent account, go to developer's guide on how to use Prepayment.
Temporary Account allows user to pay directly for Request-Response without any extra prerequisites. This approach is great for infrequent use, or for users that do not want to hassle with Temporary Account settings and want to use Request-Response as soon as possible.
In this document, we describe both Permanent Account and Temporary Account approaches that can be used for requesting data from off-chain. Finally, we explain how to build an on-chain requests and how to post-process an API response.
We assume that at this point you have already created permanent account through
Prepayment
smart contract, deposited $KLAY, and assigned consumer(s) to it. If not, please read how to do all the above, in order to be able to continue in this guide.After you created account (and obtained
accId
), deposited some $KLAY and assigned at least one consumer, you can use it to request data and receive response.User smart contract that wants to utilize Orakl Network Request-Response has to inherit from abstract fulfillment contracts to support a specific return data type. Currently, we provide the following:
uint128
withRequestResponseConsumerFulfillUint128
int256
withRequestResponseConsumerFulfillInt256
bool
withRequestResponseConsumerFulfillBool
string
withRequestResponseConsumerFulfillString
bytes32
withRequestResponseConsumerFulfillBytes32
bytes
withRequestResponseConsumerFulfillBytes
All of the above are defined within a RequestResponseConsumerFulfill file. For the sake of this tutorial, we will demonstrate with
RequestResponseConsumerFulfillUint128
only, but the same principles can be applied to other return data types.import { RequestResponseConsumerFulfillUint128 } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerFulfill.sol";
contract RequestResponseConsumer is RequestResponseConsumerFulfillUint128 {
...
}
Request-Response smart contract (
RequestResponseCoordinator
) is used both for requesting and receiving data. Address of deployed RequestResponseCoordinator
is used for initialization of parent class RequestResponseConsumerBase
.import { RequestResponseConsumerFulfillUint128 } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerFulfill.sol";
import { RequestResponseConsumerBase } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerBase.sol";
contract RequestResponseConsumer is RequestResponseConsumerFulfillUint128 {
constructor(address coordinator) RequestResponseConsumerBase(coordinator) {
}
}
The
estimateFee
function calculates the estimated service fee for a request based on the provided parameters.function estimateFee(
uint64 reqCount,
uint8 numSubmission,
uint32 callbackGasLimit
) public view returns (uint256) {
uint256 serviceFee = calculateServiceFee(reqCount) * numSubmission;
uint256 maxGasCost = tx.gasprice * callbackGasLimit;
return serviceFee + maxGasCost;
}
Let's understand the purpose and arguments of this function:
reqCount
: This is auint64
value representing the number of previous requests made. By providing theaccId
, you can obtain thereqCount
by invoking the external functiongetReqCount()
of thePrepayment contract
numSubmission
: This is auint8
value representing the number of submissions for the request.callbackGasLimit
: This is auint32
value representing the gas limit allocated for the callback function.
By calling the
estimateFee()
function with the appropriate arguments, users can get an estimation of the total fee required for their request. This can be useful for spending required amount for each request.Data request (
requestData
) must be called from a contract that has been approved through addConsumer
function of Prepayment
smart contract. If the smart contract has not been approved, the request is rejected with InvalidConsumer
error. If account (specified by accId
) does not exist (InvalidAccount
error) or does not have balance high enough, request is rejected as well.The example code below encodes a request for an ETH/USD price feed from https://min-api.cryptocompare.com/ API server. The request describes where to fetch data (https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD), and how to parse (
path
and pow10
) the response from API server (shortened version displayed in listing below). The response comes as a nested JSON dictionary on which we want to access RAW
key at first, then ETH
key, USD
key, and finally PRICE
key.{
"RAW": {
"ETH": {
"USD": {
"PRICE": 1754.02
}
}
}
}
After accessing the ETH/USD price, we notice that the price value is encoded in floating point. To simplify transition of floating point value from off-chain to on-chain, we decide to multiply the price value by 10e8 and keep the value in
uint256
data type. This final value is submitted by the off-chain oracle to RequestResponseCoordinator
which consequently calls fulfillDataRequest
function in your consumer smart contract. In the final section of this page, you can learn more about other ways how to build a request and how to parse the response from off-chain API server.function requestData(
uint64 accId,
uint32 callbackGasLimit
)
public
onlyOwner
returns (uint256 requestId)
{
bytes32 jobId = keccak256(abi.encodePacked("uint128"));
uint8 numSubmission = 1;
Orakl.Request memory req = buildRequest(jobId);
req.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD");
req.add("path", "RAW,ETH,USD,PRICE");
req.add("pow10", "8");
requestId = COORDINATOR.requestData(
req,
callbackGasLimit,
accId,
numSubmission
);
}
Below, you can find an explanation of
requestData
function and its arguments defined at RequestResponseCoordinator
smart contract:req
: aRequest
structure that holds encoded user requestcallbackGasLimit
: auint32
value representing the gas limit for the callback function that executes after the confirmations have been received.accId
: auint64
value representing the ID of the account associated with the request.numSubmission
: requested number of submission from off-chain oracles
The function call
requestData()
on COORDINATOR
contract passes req
, accId
, callbackGasLimit
and numSubmission
as arguments. After a successful execution of this function, you obtain an ID (requestId
) that uniquely defines your request. Later, when your request is fulfilled, the ID (requestId
) is supplied together with response to be able to make a match between requests and fulfillments when there is more than one request.fulfillDataRequest
is a virtual function of RequestResponseConsumerFulfillUint128
abstract smart contract (every RequestResponseConsumerFulfill*
defines this function), and therefore must be overridden. This function is called by RequestResponseCoordinator
when fulfilling the request. callbackGasLimit
parameter defined during data request denotes the amount of gas required for execution of this function.function fulfillDataRequest(
uint256 /*requestId*/,
uint128 response
)
internal
override
{
sResponse = response;
}
The arguments of
fulfillDataRequest
function are explained below:requestId
: auint256
value representing the ID of the requestresponse
: anuint128
value that was obtained after processing data request sent fromrequestData
function
This function is executed from
RequestResponseCoordinator
contract defined during smart contract initialization. The result is saved in the storage variable sResponse
.Temporary Account is an alternative type of account which does not require a user to create account, deposit $KLAY, or assign consumer before being able to utilize Request-Response functionality. Request-Response with Temporary Account is only a little bit different compared to Permanent Account, however, the fulfillment function is exactly same.
User smart contract that wants to utilize Orakl Network Request-Response has to inherit from one of the following abstract smart contracts that define what data type we expect as a response.
uint128
withRequestResponseConsumerFulfillUint128
int256
withRequestResponseConsumerFulfillInt256
bool
withRequestResponseConsumerFulfillBool
string
withRequestResponseConsumerFulfillString
bytes32
withRequestResponseConsumerFulfillBytes32
bytes
withRequestResponseConsumerFulfillBytes
import { RequestResponseConsumerFulfillUint128 } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerFulfill.sol";
contract RequestResponseConsumer is RequestResponseConsumerFulfillUint128 {
...
}
There is no difference in initialization of Request-Response consumer contract that requests for data with permanent or temporary account.
Request-Response smart contract (
RequestResponseCoordinator
) is used both for requesting and receiving data. Address of deployed RequestResponseCoordinator
is used for initialization of parent class RequestResponseConsumerBase
.import { RequestResponseConsumerFulfillUint128 } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerFulfill.sol";
import { RequestResponseConsumerBase } from "@bisonai/orakl-contracts/src/v0.1/RequestResponseConsumerBase.sol";
contract RequestResponseConsumer is RequestResponseConsumerFulfillUint128 {
constructor(address coordinator) RequestResponseConsumerBase(coordinator) {
}
}
The data request using Temporary Account is very similar to request using Permanent Account. The only difference is that for Temporary Account user has to send $KLAY together with call using
value
property, and does not have to specify account ID (accId
) as in Permanent Account. There are several checks that have to pass in order to successfully request data. You can read about them in one of the previous subsections called Request data.receive() external payable {}
function requestData(
uint32 callbackGasLimit,
address refundRecipient
)
public
payable
onlyOwner
returns (uint256 requestId)
{
bytes32 jobId = keccak256(abi.encodePacked("uint128"));
uint8 numSubmission = 1;
Orakl.Request memory req = buildRequest(jobId);
req.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD");
req.add("path", "RAW,ETH,USD,PRICE");
requestId = COORDINATOR.requestData{value: msg.value}(
req,
callbackGasLimit,
numSubmission,
refundRecipient
);
}
This function calls the
requestData()
function defined in COORDINATOR
contract, and passes req
, callbackGasLimit
, numSubmission
and refundRecipient
as arguments. The payment for service is sent through msg.value
to the requestData()
in COORDINATOR
contract. If the payment is larger than expected, the exceeding amount is returned to the refundRecipient
address. Eventually, it generates a data request.In the section below, you can find more detailed explanation of how data request using temporary account works.
In the previous section, we explained that $KLAY is sent together with request for data to
RequestResponseCoordinator
which passes the $KLAY deposit to Prepayment
contract. The $KLAY payment stays in the Prepayment
contract until the request is fulfilled.In rare cases, it is possible that request cannot be fulfilled, and consumer does not receive requested data. To refund deposited $KLAY in such cases, one must first cancel request by calling
cancelRequest
inside of RequestResponseCoordinator
and then withdraw $KLAY (withdrawTemporary
) from temporary account inside of Prepayment
contract. In both cases, consumer smart contract has to be the sender (msg.sender
). Our consumer smart contract therefore has to include such auxiliary function(s) to make appropriate calls. If we do not add such functions to consumer contract, it will not be possible to cancel request and withdraw funds deposited to temporary account. Deposited funds will be then forever locked inside of Prepayment
contract.The code listing below is an example of function inside of consumer contract to cancel and withdraw funds from temporary account.
function cancelAndWithdraw(
uint256 requestId,
uint64 accId,
address refundRecipient
) external onlyOwner {
COORDINATOR.cancelRequest(requestId);
address prepaymentAddress = COORDINATOR.getPrepaymentAddress();
IPrepayment(prepaymentAddress).withdrawTemporary(accId, payable(refundRecipient));
}
function requestData(
Orakl.Request memory req,
uint32 callbackGasLimit,
uint8 numSubmission,
address refundRecipient
) external payable nonReentrant returns (uint256) {
uint64 reqCount = 0;
uint256 fee = estimateFee(reqCount, numSubmission, callbackGasLimit);
if (msg.value < fee) {
revert InsufficientPayment(msg.value, fee);
}
uint64 accId = sPrepayment.createTemporaryAccount(msg.sender);
bool isDirectPayment = true;
uint256 requestId = requestData(
req,
accId,
callbackGasLimit,
numSubmission,
isDirectPayment
);
sPrepayment.depositTemporary{value: fee}(accId);
// Refund extra $KLAY
uint256 remaining = msg.value - fee;
if (remaining > 0) {
(bool sent, ) = refundRecipient.call{value: remaining}("");
if (!sent) {
revert RefundFailure();
}
}
return requestId;
}
This function first calculates a fee (
fee
) for the request by calling estimateDirectPaymentFee()
function. isDirectPayment
variable indicates whether the request is created through Prepayment or Direct Payment method. Then, it deposits the required fee (fee
) to the account by calling s_prepayment.deposit(accId)
and passing the fee (fee
) as value. If the amount of $KLAY passed by msg.value
to the requestData
is larger than required fee (fee
), the remaining amount is sent back to the caller using the msg.sender.call()
method. Finally, the function returns requestId
that is generated by the requestDataInternal()
function.This function first calculates a fee for the request by calling
estimateFee()
function. Then, it create a temporary account inside of Prepayment contract with sPrepayment.createTemporaryAccount(msg.sender)
call. In the next step, we request data by calling requestData
function. The function has several validation steps, therefore we included requesting for data before depositing the required fee to the account (sPrepayment.depositTemporary{value: fee}(accId)
). If the amount of $KLAY passed by msg.value
to the requestData
is larger than a required fee, the remaining amount is sent back to the refundRecipient
address. Finally, the function returns requestId
that is generated by the internal requestData()
call.The Orakl Network Request-Response solution enables consumers to define their own requests on-chain, process them by off-chain oracle, and report the results back to consumer smart contract on chain.
Requests are created with the help of the Orakl library. Every request is associated with a job ID which describes what data type it expects to report. The list of currently supported report data types can be found in the table below.
Response Data Type | Job ID |
---|---|
uint128 | keccak256(abi.encodePacked("uint128") |
int256 | keccak256(abi.encodePacked("int256") |
boolean | keccak256(abi.encodePacked("boolean") |
bytes32 | keccak256(abi.encodePacked("bytes32") |
bytes | keccak256(abi.encodePacked("bytes") |
string | keccak256(abi.encodePacked("string") |
The job identifier is used to initialize the
Orakl.Request
data structure. Once the request is received by the off-chain oracle, it knows what data type to use for final reporting.bytes32 jobId = keccak256(abi.encodePacked("uint128"));
Orakl.Request memory req = buildRequest(jobId);
Instance of the
Orakl.Request
holds all information about consumer's API request and how to post-process the API response. Both request, and post-processing details are inserted to the instance of Orakl.Request
using add
function. The add
function accepts key-value pair parameters, where the first one represents the type of data passed, and the second one is the data itself. The first inserted key has to be get
, and the value has to be a valid API URL.req.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD");
If the first key is notget
with valid API URL link as a value, then the request will fail and will not be processed.
The Orakl Network Request-Response currently supports five different post-processing operations that are listed in the table below.
Operation name | Explanation | Example |
---|---|---|
path | list of keys for walk through input JSON | req.add("RAW,ETH,USD,PRICE") |
index | Array index | req.add("index", "2"); |
mul | Multiplication | req.add("mul", "2"); |
div | Division | req.add("div", "2"); |
pow10 | Multiplication by | req.add("pow10", "8"); |