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
- 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. - 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, andsaveQuery
to write back the query results to the user's smart contract. - The SxTRelay contract emits an event for this request with the user contract address,
requestId
, and URL. - The SxTRelay contract requests data to a specific Chainlink Node through the Chainlink Operator Contract, specifying the data source and request parameters.
- 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.
- The SxT Validator layer sends the query request to the SxT Data Warehouse clusters, where the entire blockchain history is stored.
- 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.
- The Chainlink node returns the data to the Chainlink Operator smart contract with a transaction.
- 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.
- 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
- Firstnamespace.table
name used by the SQL query to route the data from Validator layer
Execute SQL query through SxTRelay
contract
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 thecurrentRequestId
-
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
-
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
- 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/
- 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"
- 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:
Name | Address |
---|---|
SxTChainlinkOperator: | 0xecd41Fd32C8E25dc2ca86D4528d966bA0Cdf58b8 |
SxTPublishDataFactory: | 0xf68a54ff4580d2B12E5fC45c31Fa4aea3e981e78 |
SxTRelayProxy: | 0xf6b18242dab7af6F7390505fCFd16e03F61F8bCB |
UserRequestBytes: | 0x57fBbCBABfa409D6C5E75387080c9a07aa714f5c |
UserRequestString: | 0x00962Fc5168f83278F1dD32543C335b53a5393D5 |
UserRequestString2D: | 0xbc4Cb22ce56fA8869096Af9E3E87a94d9396440E |
UserRequestUint256: | 0xaCBB7771d778aAd35f5b46E26E2639cBc45Dd27F |
Current JobIds
Query
Data Type | Id |
---|---|
Bytes: | bc97c680d2924f31a0581d947314cc62 |
String: | bc97c680d2924f31a0581d947314cc63 |
String2D: | bc97c680d2924f31a0581d947314cc64 |
Uint256: | bc97c680d2924f31a0581d947314cc61 |
View
Data Type | Id |
---|---|
Bytes: | bc97c680d2924f31a0581d947314cc72 |
String: | bc97c680d2924f31a0581d947314cc73 |
String2D: | bc97c680d2924f31a0581d947314cc84 |
Uint256: | bc97c680d2924f31a0581d947314cc71 |
Contract Addresses Deployed on Goerli
Environment | Contract | Address |
---|---|---|
Goerli | ChainlinkOperator Contract | 0x1EF964d2680fF5d346c8aE8D5A8dfcF7DFFAC202 |
Goerli | SXTRelay | 0x07196eac2f0F60499D924d998E79457E4e7714b1 |
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"}]
Updated 12 months ago