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);