Example Contract for Oracle Integration
This example contract illustrates how to interact with an Oracle. Clients can use this code as a reference to create their own contract based on this model.
Contract Code
Below is the code for the ExampleContract.sol
contract:
// SPDX-License-Identifier: MIT
// Example Contract
pragma solidity ^0.8.19;
import {OracleRequest} from "./oracle/lib/OracleRequest.sol";
import {OracleClient} from "./oracle/OracleClient.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; //not sure if this is needed
contract ExampleContract is OracleClient, ReentrancyGuard {
using OracleRequest for OracleRequest.Request;
address public oracleRouter;
struct RequestData {
uint256 exampleUint256;
address exampleAddress;
}
mapping(uint256 requestId => RequestData request)
public s_requestIdToRequest;
bytes public exampleFulfillResponseBytes;
uint256 public exampleFulfillResponseUint256;
string public testerCustomBody;
constructor(address oracleRouterAddress) OracleClient(oracleRouterAddress) {
oracleRouter = oracleRouterAddress;
}
// First calculate the gas units for the callback off-chain
function exampleSendRequestPOST(
uint256 fulfillGasUsed,
string memory clientId,
string memory name
) external payable nonReentrant {
uint256 gasCost = gasCostFulfill(fulfillGasUsed);
require(
msg.value > gasCost,
"ETH sent is less than gas cost for the callback"
);
string memory url = "http://<your host>:<port>/api/data/";
string memory requestBody = string(
abi.encodePacked(
'{"clientId":"',
clientId,
'","name":"',
name,
'"}'
)
);
OracleRequest.Request memory req;
req.url = url; //requiered
req.method = "POST"; //required
req.slot = "0"; // <-- SELECT SLOT YOU WANT TO USE FOR THIS REQUEST
req.jsonResponsePath = "data.result";
req.requestBody = requestBody;
testerCustomBody = req.requestBody;
req.bodyValuesDataTypes = '["uint256","string"]'; // remember double quote in elements of string array for match json format
//req.allowFloatResponse = true; <-- uncomment this line if you are specting a float to return (IF YOU DO, BOTH UINT AND FLOAT WIL BE MUL BY 10**18 BEFORE SENDING THE ORACLE RESPONSE)
bytes memory requestData = req.encodeCBOR();
//this will emit a event on OracleRouter that nodes will caputure
//msg.value is sent here
uint256 requestId = _sendRequest(requestData, fulfillGasUsed);
// Dummy data to show how to save the requestId of this request, for later build your code
// inside fulfillRequest() based on the same requestId
uint256 dummyUint256 = 11;
address dummyAddress = address(0);
s_requestIdToRequest[requestId] = RequestData(
dummyUint256,
dummyAddress
);
}
function exampleSendRequestGET(
uint256 fulfillGasUsed,
string memory name,
string memory age
) external payable nonReentrant {
uint256 gasCost = gasCostFulfill(fulfillGasUsed);
require(
msg.value > gasCost,
"ETH sent is less than gas cost for the callback"
);
// Base URL
string memory baseUrl = "http://<your host>:<port>/greet/";
// Concatenate the URL with the name and age
string memory url = string(abi.encodePacked(baseUrl, name, "/", age));
OracleRequest.Request memory req;
req.url = url;
req.method = "GET";
req.slot = "0";
req.jsonResponsePath = "message"; // Adjust this according to the actual response structure
bytes memory requestData = req.encodeCBOR();
//this will emit a event on OracleRouter that nodes will caputure
//msg.value is sent here
uint256 requestId = _sendRequest(requestData, fulfillGasUsed);
}
// THIS FUNCTION IS CALLED BY THE ORACLE
// oracle will call fulfill() on the OracleRouter address
function fulfillRequest(
uint256 requestId,
bytes memory response
) internal override {
exampleFulfillResponseBytes = response;
}
function decodeBytesToUint256() public view returns (uint256) {
uint256 value = abi.decode(exampleFulfillResponseBytes, (uint256));
return value;
}
function decodeBytesToString() public view returns (string memory) {
string memory value = string(exampleFulfillResponseBytes);
return value;
}
function decodeBytesToBool() public view returns (bool) {
bool value = abi.decode(exampleFulfillResponseBytes, (bool));
return value;
}
function gasCostFulfill(
uint256 fulfillGasUsed
) public view returns (uint256) {
uint256 gasPrice = tx.gasprice;
return fulfillGasUsed * gasPrice;
}
}
Let's start by analyzing the sendRequestPost()
function
As you can see, the function is payable
. Why is that? This is because EyeOracle, unlike other oracles that use subscription-based systems to cover the gas cost for the oracle callback, allows the user to pay directly for the callback.
This is a different approach, but we believe it's necessary since the user who calls sendRequestPost()
should cover all the costs involved in that transaction.
How to know the gas used in the callback?
In your dashboard, there is a useful tool to simulate the callback cost once you have the contract deployed. See Gas calculations
Let's continue analyzing the request object
Now, let's break down the Request
object and understand the purpose of each attribute:
Request
object is a struc that inherits from OracaleRequest.sol
library.
struct Request {
string url;
string method;
string slot;
string jsonResponsePath;
string requestBody;
string bodyValuesDataTypes;
bool allowFloatResponse;
}
req.url
==> The URL to which the oracle's request will point.req.method
==> The method the request will use (e.g., POST, GET).req.slot
==> Memory slot position for the headers that will be used in the request. You will have configured these headers beforehand in the dashboard. See How configure secretsreq.jsonResponsePath
==> Specifies how the oracle should capture data from the response. In this case, from the JSON response, the oracle will retrieve the value fromresponse.data.result
.req.allowFloatResponse
==> DEFAUT=flase, Allow oracle to float reponses, IMPORTANT if you pass req.allowFloatResponse=true, both uint and float responses wil be multiplicated by 10**18 before sending to your contract.req.requestBody
==> The request body, which is a JSON string that allows you to configure your key-value pairs to be incorporated into the body of the request. All input values MUST be strings, but withreq.bodyValuesDataTypes
you can specify the type of data being input.req.bodyValuesDataTypes
==> JSON string of the data type of the values you are sending in the body, enabling the oracle to transform them before making the API request.
The correct syntaxis of a JSON string to pass to the oracle:
This is correct
'["uint256","string"]';
This is wrong
"['uint256','string']";
Note --> If you chose uint256 but you number is too big (BigInt) oracle will auto convert to string. Example if you pass 5 and you set 5 as uint256 oracle will use 5 as number class, but if you pass 5*10**18 oracle will auto transform to string class even if you use uint256. This is due JSON problems to parse BigInts
Finally, when all the req
parameters are set, you can encode them for the oracle by calling req.encodeCBOR()
.