Fetching multiple off-chain values from a smart contract
Apr 25, 2022 - #chainlink#oracles#smart-contracts
Being able to fetch and return multiple values from an off-chain API using Chainlink’s AnyAPI has been tricky in the past. The use of the older Oracle.sol contract by node operators limited the size of the return response of the calling smart contract. Workarounds involved sending back multiple responses with the side effect of increased gas costs.
The use of the newer Operator.sol contract for node operator makes larger responses a possibility, hence multi-variable responses are now also possible.
The first step is for the node operator to deploy Operator.sol, for which you’ll find instructions in this sister post’s “Oracles and Operators” section.
Get > Uint256, Uint256
Our aim is to call this price API and return the upper and lower values from the payload below:
{
upper: 98825500000000000000,
price: 93821290000000010000,
lower: 88817080000000020000,
priceUnit: "ETH_WEI",
generatedFor: "2022-04-19T16:28:50.000Z"
}
Assuming the node operator has deployed Operator.sol, the next step is to convert Get > Uint256
into Get > Uint256, Uint256
.
The node operator needs deploy a job similar to the one below. Pay special attention to the two contract addresses references, explained next.
type = "directrequest"
schemaVersion = 1
name = "Get > Uint256, Uint256"
maxTaskDuration = "0s"
contractAddress = "0x1314E350Fc5a3896E2d66C43A83D9391E914a004"
minIncomingConfirmations = 0
observationSource = """
decode_log [type="ethabidecodelog"
abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)"
data="$(jobRun.logData)"
topics="$(jobRun.logTopics)"]
decode_cbor [type="cborparse" data="$(decode_log.data)"]
fetch [type="http" method=GET url="$(decode_cbor.get)"]
parseUpper [type="jsonparse" path="$(decode_cbor.pathUpper)" data="$(fetch)"]
parseLower [type="jsonparse" path="$(decode_cbor.pathLower)" data="$(fetch)"]
multiplyUpper [type="multiply" input="$(parseUpper)" times="$(decode_cbor.times)"]
multiplyLower [type="multiply" input="$(parseLower)" times="$(decode_cbor.times)"]
encode_data [type="ethabiencode" abi="(bytes32 requestId, uint256 lower, uint256 upper)" data="{ \"requestId\": $(decode_log.requestId), \"lower\": $(multiplyLower), \"upper\": $(multiplyUpper)}"]
encode_tx [type="ethabiencode"
abi="fulfillOracleRequest2(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes calldata data)"
data="{\"requestId\": $(decode_log.requestId), \"payment\": $(decode_log.payment), \"callbackAddress\": $(decode_log.callbackAddr), \"callbackFunctionId\": $(decode_log.callbackFunctionId), \"expiration\": $(decode_log.cancelExpiration), \"data\": $(encode_data)}"
]
submit_tx [type="ethtx" to="0x1314E350Fc5a3896E2d66C43A83D9391E914a004" data="$(encode_tx)"]
decode_log -> decode_cbor -> fetch -> parseUpper -> multiplyUpper -> parseLower -> multiplyLower -> encode_data -> encode_tx -> submit_tx
"""
The two contract addresses (both 0x1314..004
above) need to point to the deployed Operator contract.
Also note the encode_data
step which returns the requestId
, and the two uint256
values.
Finally, the encode_tx
step uses fulfillOracleRequest2()
whereby the final parameter is the bytes calldata
data payload being sent back the callback handler on the calling smart contract.
Fetching two values using a smart contract
To call the job on the node via the operator contract follow the example below:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";
contract MultiVariableRequest is ChainlinkClient, ConfirmedOwner {
using Chainlink for Chainlink.Request;
uint256 constant private ORACLE_PAYMENT = 1 * LINK_DIVISIBILITY / 100 * 5;
uint256 public lower;
uint256 public upper;
constructor() ConfirmedOwner(msg.sender){
setChainlinkToken(0x326C977E6efc84E512bB9C30f76E30c160eD06FB);
setChainlinkOracle(0x1314E350Fc5a3896E2d66C43A83D9391E914a004);
}
function requestUpperAndLower(string memory _jobId)
public
onlyOwner
{
Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), address(this), this.fulfillUpperAndLower.selector);
req.add("get", "https://api.tensor.so/eth/collections/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/floor?at=1650706413");
req.add("pathUpper", "upper");
req.add("pathLower", "lower");
req.addInt("times", 100);
sendOperatorRequest(req, ORACLE_PAYMENT);
}
event RequestFulfilledUpperAndLower(
bytes32 indexed requestId,
uint256 indexed lower,
uint256 indexed upper
);
function fulfillUpperAndLower(
bytes32 requestId,
uint256 _lower,
uint256 _upper
)
public
recordChainlinkFulfillment(requestId)
{
emit RequestFulfilledUpperAndLower(requestId, _lower, _upper);
upper = _upper;
lower = _lower;
}
function stringToBytes32(string memory source) private pure returns (bytes32 result) {
bytes memory tempEmptyStringTest = bytes(source);
if (tempEmptyStringTest.length == 0) {
return 0x0;
}
assembly { // solhint-disable-line no-inline-assembly
result := mload(add(source, 32))
}
}
}
Important items to note that differ from the canonical Get > Uint256
example are the use of
setChainlinkOracle(0x1314E350Fc5a3896E2d66C43A83D9391E914a004);
in the constructor which points to the Operator contract address.
Finally note how Operator.sol requires us to send requests with sendChainlinkRequestTo()
in requestBytes()
:
sendOperatorRequest(req, ORACLE_PAYMENT);