Publish to a Smart Contract (Chainlink Direct Requests)

Publishing SxT query results on-chain using Chainlink Direct Requests.

🚧

If you're new to Space and Time, we recommend following the Getting Started Guide to get familiar with the basic concepts first.

What are Chainlink Direct Requests?

Chainlink Direct Requests (DR) allow smart contracts to access external data and APIs in a secure and decentralized way. The smart contract asks a specific Chainlink node to retrieve a specific piece of data from a specified source. The node then fetches the data and returns it to the contract, which can then use it to execute its programmed logic.

📘

Note on security

DRs can be used to access a wide variety of data sources. The security of the data source will affect the overall security of the DR, i.e. if the data source is secure and reliable, the data returned by the Chainlink node will also be secure. If the data source is vulnerable to tampering or other security risks, it could compromise the security of the DR.

Space and Time provides a secure and reliable data source by cryptographically guaranteeing the results of queries run in the SxT data warehouse. This opens up a wealth of new opportunities for smart contract developers to program complex business logic by integrating analytics against both on-chain and off-chain data in a completely secure and tamperproof way.

Overview of publishing a SxT query with Direct Requests

  1. A user deploys a smart contract on the blockchain containing code (SQL) that defines the data it needs to access and the terms of use. The user makes an API request to the SxTRelay contract with the requestId and query parameters, and pays a minimum amount to send the request.
  2. The SxTRelay contract takes the user's address as a unique identifier. It uses the existing Chainlink Client Request contract and has methods, such as requestQuery, to send requests to the Chainlink operator, and saveQuery to write back the query results to the user's smart contract.
  3. The SxTRelay contract emits an event for this request with the user contract address, requestId, and URL.
  4. The SxTRelay contract requests data to a specific Chainlink Node through the Chainlink Operator Contract, specifying the data source and request parameters.
  5. The Chainlink Node receives the request by listening to the emitted event and fetches the data from the specified source. It requests data from the External Adapter written by SxT. This adapter makes an API call directly to the SxT Validator layer to retrieve the query result from the SxT Data Warehouse clusters.
  6. The SxT Validator layer sends the query request to the SxT Data Warehouse clusters, where the entire blockchain history is stored.
  7. The External Adapter retrieves the information, and one of the Chainlink Jobs tasks parses the JSON query result and encodes the data in a 2D array.
  8. The Chainlink node returns the data to the Chainlink Operator smart contract with a transaction.
  9. The Chainlink Operator sends the data to the User smart contract through the SxTRelay contract with a callback function and contract address. The SxTRelay contract saves the request back to the User contract, which can then use it to execute its programmed logic. Essentially, the Chainlink operator node generates a report and makes a Web3 call to send this report to the smart contract.
  10. The smart contract processes the data and may perform additional actions, such as updating its internal state or triggering other smart contracts.

Contracts

UserRequestDemo

Sample user contract that inherits UserRequest abstract contract used to send request and record its fulfilled oracle request.

  • This Contract is deployed by each project/smart contract owner.
  • It should inherit UserRequest:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./SxTRequest.sol";
import "./interfaces/ChainlinkTokenInterface.sol";

/**
 * @title User Request contract
 */
abstract contract UserRequest {

    // Zero Address
    address constant ZERO_ADDRESS = address(0);

    /// @dev SxT Request contract address
    address public sxtRequestContract;

    /// @dev Chainlink token address
    ChainlinkTokenInterface public chainlinkToken;

    /// @dev Current request Id
    bytes32 public currentRequestId;

    /** 
     * @dev The constructor sets the SxTRequest and validator contract address
     * @param sxtRequestAddress - Address of the SxT request contract address that has Oracle and Job initialized on it
     * @param chainlinkTokenAddress - Address of the LINK token that would be used for payment
     */
    constructor (address sxtRequestAddress, ChainlinkTokenInterface chainlinkTokenAddress) {
        require(sxtRequestAddress != ZERO_ADDRESS, "UserRequest: Cannot set to Zero Address");
        require(chainlinkTokenAddress != ChainlinkTokenInterface(ZERO_ADDRESS), "UserRequest: Cannot set to Zero Address");
        sxtRequestContract = sxtRequestAddress;
        chainlinkToken = chainlinkTokenAddress;
    }

    /**
     * @dev Modifier to constraint only the SxTRequest contract to call the function
     */
    modifier onlySxTRequest() {
        require(msg.sender == sxtRequestContract, "Only callable by SxT Request Contract");
        _;
    }

    /**
     * @dev triggers the requestQuery function of the SxTRequest contract
     * @param resourceId - request id
     * @param query - user query
     */
    function runSxTRequest(string memory query, string memory resourceId) external returns(bytes32 requestId){
        SxTRequest sxtRequestInstance = SxTRequest(sxtRequestContract);
        require(chainlinkToken.approve(sxtRequestContract, sxtRequestInstance.FEE()), "SxTRequest: insufficient allowance");
        return bytes32(abi.encodePacked(sxtRequestInstance.requestQuery(address(this), query, resourceId)));
    }

    /**
     * @dev The node calls this function to write the result of the query
     * @param requestId - request id
     * @param data - response of the user query
     */
    function saveQueryResponse(bytes32 requestId, string[][] calldata data) external virtual onlySxTRequest {}

    /**
     * @dev Withdraw Chainlink from contract
     * @param to - Address to transfer the LINK tokens
     * @param amount - Amount of the LINK tokens to transfer
     */
    function withdrawChainlink(address to, uint256 amount) external {
        bool transferResult = chainlinkToken.transfer(
            to,
            amount
        );
        require(transferResult, "SxTRequest: Chainlink token transfer failed");
    }
}
  • The request will be sent to the SxTRelay contract, and a response will be received back to this contract.

Smart contract functions

constructor (address sxtRequestAddress, ChainlinkTokenInterface chainlinkTokenAddress)

  • sxtRequestAddress - Address of the SxT request contract address that has Oracle and Job initialized on it

  • chainlinkTokenAddress - Address of the LINK token that would be used for payment

  • This function can be called by the contract owner.

runSxTRequest(string memory query, string memory resourceId) external returns(bytes32 requestId)

  • query - SQL query requested from SxT

  • resourceId - First namespace.table name used by the SQL query to route the data from Validator layer

Execute SQL query through SxTRelay contract

  • resourceId - used to identify Data Warehouse cluster in Validator layer. Typically the first table name used in the SQL query

  • sqlText is the SQL query that will be executed on the SxT

  • This function can be called by the contract owner.

saveResponse(bytes32 requestId, string[][] response)

  • Executed by the SxTRelay, returns the data back from the Chainlink node operator

  • Updates the currentResponse data

  • requestId has to be equal to the currentRequestId

  • This function can only be called by the Chainlink node operator.

🚧

The rest of the documentation is based on running on top of SxT Alpha-DEV clusters and Goerli Network. Ethereum mainnet is not yet supported.

User Sample Deployment

  1. Deploy the UserRequestDemo contract. (Please see the Chainlink Operator and SxTRequest contract addresses that are deployed by the SxT below charts).

    Contract sample for users:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./UserRequest.sol";
import "./ConfirmedOwner.sol";

/**
 * @title User Request contract
 */
contract UserRequestDemo is UserRequest, ConfirmedOwner {

    /// @dev Chainlink Request call response
    string[][] public currentResponse;

    /** 
     * @dev The constructor sets the SxTRequest and validator contract address
     * @param sxtRequestAddress - SxT request contract address
     * @param chainlinkTokenAddress - Chainlink Token address
     */
    constructor (address sxtRequestAddress, ChainlinkTokenInterface chainlinkTokenAddress)
        UserRequest(sxtRequestAddress, chainlinkTokenAddress)
        ConfirmedOwner(msg.sender)
    {}

    /**
     * @dev The node calls this function to write the result of the query
     * @dev The SxT request contract will be looking for the function name saveQueryResponse for saving the response
     * @param requestId - request id
     * @param data - response of the user query
     */
    function saveQueryResponse(bytes32 requestId, string[][] calldata data) external override {
        delete currentResponse;
        currentRequestId = requestId;
        // Store response
        for (uint256 i = 0; i < data.length; i++) {
            uint256 inLength = data[i].length;
            string[] memory row = new string[](inLength);
            for (uint256 j = 0; j < inLength; j++) {
                row[j] = data[i][j];
            }
            currentResponse.push(row);
        }
    }
}
$ npx hardhat deploy:UserRequestDemo --network goerli
$ npx hardhat verify:UserRequestDemo --network goerli
  1. Transfer LINK tokens to the UserRequestDemo contract for payment of the desired SQL queries. Use the faucet links provided for this purpose.
Faucet Link: https://goerlifaucet.com/, https://goerli-faucet.pk910.de/
  1. Call the UserRequestDemo contract function to retrieve the response from the SxT databases. Pass the desired query and resourceId as parameters.
$ npx hardhat action:UserRequestDemo:runSxTRequest --resourceid "ETH.WALLET"
  1. Check the response in the UserRequestDemo response function currentResponse. Pass the required array indexes to retrieve the desired response.
$ npx hardhat action:UserRequestDemo:currentResponse --inputrow 0 --inputcolumn 0

Current deployed contracts on Mumbai:

NameAddress
SxTChainlinkOperator:0xecd41Fd32C8E25dc2ca86D4528d966bA0Cdf58b8
SxTPublishDataFactory:0xf68a54ff4580d2B12E5fC45c31Fa4aea3e981e78
SxTRelayProxy:0xf6b18242dab7af6F7390505fCFd16e03F61F8bCB
UserRequestBytes:0x57fBbCBABfa409D6C5E75387080c9a07aa714f5c
UserRequestString:0x00962Fc5168f83278F1dD32543C335b53a5393D5
UserRequestString2D:0xbc4Cb22ce56fA8869096Af9E3E87a94d9396440E
UserRequestUint256:0xaCBB7771d778aAd35f5b46E26E2639cBc45Dd27F

Current JobIds

Query

Data TypeId
Bytes:bc97c680d2924f31a0581d947314cc62
String:bc97c680d2924f31a0581d947314cc63
String2D:bc97c680d2924f31a0581d947314cc64
Uint256:bc97c680d2924f31a0581d947314cc61

View

Data TypeId
Bytes:bc97c680d2924f31a0581d947314cc72
String:bc97c680d2924f31a0581d947314cc73
String2D:bc97c680d2924f31a0581d947314cc84
Uint256:bc97c680d2924f31a0581d947314cc71

Contract Addresses Deployed on Goerli

EnvironmentContractAddress
GoerliChainlinkOperator Contract0x1EF964d2680fF5d346c8aE8D5A8dfcF7DFFAC202
GoerliSXTRelay0x07196eac2f0F60499D924d998E79457E4e7714b1

ABIs

ChainlinkOperator

[{"inputs":[{"internalType":"address","name":"_chainlink","type":"address"},{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address[]","name":"senders","type":"address[]"},{"indexed":false,"internalType":"address","name":"changedBy","type":"address"}],"name":"AuthorizedSendersChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"requestId","type":"bytes32"}],"name":"CancelOracleRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"specId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"requester","type":"address"},{"indexed":false,"internalType":"bytes32","name":"requestId","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"payment","type":"uint256"},{"indexed":false,"internalType":"address","name":"callbackAddr","type":"address"},{"indexed":false,"internalType":"bytes4","name":"callbackFunctionId","type":"bytes4"},{"indexed":false,"internalType":"uint256","name":"cancelExpiration","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"dataVersion","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"OracleRequest","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"requestId","type":"bytes32"}],"name":"OracleResponse","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"acceptedContract","type":"address"}],"name":"OwnableContractAccepted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"OwnershipTransferRequested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address[]","name":"targets","type":"address[]"},{"indexed":false,"internalType":"address[]","name":"senders","type":"address[]"},{"indexed":false,"internalType":"address","name":"changedBy","type":"address"}],"name":"TargetsUpdatedAuthorizedSenders","type":"event"},{"inputs":[{"internalType":"address[]","name":"targets","type":"address[]"},{"internalType":"address[]","name":"senders","type":"address[]"}],"name":"acceptAuthorizedReceivers","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"ownable","type":"address[]"}],"name":"acceptOwnableContracts","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"acceptOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"requestId","type":"bytes32"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"bytes4","name":"callbackFunc","type":"bytes4"},{"internalType":"uint256","name":"expiration","type":"uint256"}],"name":"cancelOracleRequest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"bytes4","name":"callbackFunc","type":"bytes4"},{"internalType":"uint256","name":"expiration","type":"uint256"}],"name":"cancelOracleRequestByRequester","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable[]","name":"receivers","type":"address[]"},{"internalType":"uint256[]","name":"amounts","type":"uint256[]"}],"name":"distributeFunds","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"requestId","type":"bytes32"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"address","name":"callbackAddress","type":"address"},{"internalType":"bytes4","name":"callbackFunctionId","type":"bytes4"},{"internalType":"uint256","name":"expiration","type":"uint256"},{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"fulfillOracleRequest","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"requestId","type":"bytes32"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"address","name":"callbackAddress","type":"address"},{"internalType":"bytes4","name":"callbackFunctionId","type":"bytes4"},{"internalType":"uint256","name":"expiration","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"fulfillOracleRequest2","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getAuthorizedSenders","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getChainlinkToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getExpiryTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"isAuthorizedSender","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"onTokenTransfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"bytes32","name":"specId","type":"bytes32"},{"internalType":"bytes4","name":"callbackFunctionId","type":"bytes4"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"dataVersion","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"operatorRequest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"payment","type":"uint256"},{"internalType":"bytes32","name":"specId","type":"bytes32"},{"internalType":"address","name":"callbackAddress","type":"address"},{"internalType":"bytes4","name":"callbackFunctionId","type":"bytes4"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"dataVersion","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"oracleRequest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"ownerForward","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"ownerTransferAndCall","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"senders","type":"address[]"}],"name":"setAuthorizedSenders","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"targets","type":"address[]"},{"internalType":"address[]","name":"senders","type":"address[]"}],"name":"setAuthorizedSendersOn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"ownable","type":"address[]"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnableContracts","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawable","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]

SXTRelay

[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"AlreadyInitialized","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"ChainlinkCancelled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"ChainlinkFulfilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"ChainlinkRequested","type":"event"},{"inputs":[],"name":"FEE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"chainlinkJobId","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes1","name":"b","type":"bytes1"}],"name":"char","outputs":[{"internalType":"bytes1","name":"c","type":"bytes1"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"currentRequestId","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getAdmin","outputs":[{"internalType":"address","name":"adminAddress","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"chainlink","type":"address"},{"internalType":"string","name":"jobId","type":"string"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"requestId","type":"bytes32"},{"internalType":"string[][]","name":"data","type":"string[][]"}],"name":"queryResponse","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"callerContract","type":"address"},{"internalType":"string","name":"query","type":"string"},{"internalType":"string","name":"resourceId","type":"string"}],"name":"requestQuery","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"adminAddress","type":"address"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"jobId","type":"string"}],"name":"setChainlinkJobID","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOperator","type":"address"}],"name":"setChainlinkOperator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_source","type":"string"}],"name":"stringToBytes32","outputs":[{"internalType":"bytes32","name":"result","type":"bytes32"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"x","type":"address"}],"name":"toAsciiString","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawChainlink","outputs":[],"stateMutability":"nonpayable","type":"function"}]