# WINkLink AnyAPI Service
# Overview
AnyAPI service provides users the ability to take advantage of the robustness of WinkLink node to generate custom data. This is enabled through the wide range of supported internal adapters.
This service helps developers to obtain data from any off-chain source and perform the necessary transformations and eventually feed it back to the chain. Such data can be airline arrival/departure timings, sporting match results, traffic conditions and many more.
The WINkLink AnyAPI solution contains both off-chain and on-chain components:
- AnyAPI Consumer (on-chain component): Crafted to interact with the AnyAPI Operator contract. User is to topup this contract with Wink tokens and initiate the request.
- AnyAPI Operator (on-chain component): A handler contract created to process all AnyAPI requests initiated from consumer contracts. It emits an event when a request is initiated and then forwards the answer back to the consumer contract.
- AnyAPI service (off-chain node): Listens for requests by subscribing to the AnyAPI operator event logs and performs designated operations to obtain custom data. Jobs are triggered based on the external job ID specified in the request.
# Contracts
A set of contracts are deployed in the nile environment for testing
Item | Value |
---|---|
WIN Token | TNDSHKGBmgRx9mDYA9CnxPx55nu672yQw2 |
WinkMid | TLLEKGqhH4MiN541BDaGpXD7MRkwG2mTro |
Operator | THRs9Y3vqE4FMTE7LPjMB4LFEH8uUsZaE4 |
SingleWordConsumer | TP3yr6MYDTDsta9JchahunXk1vvKkw87LS |
MultiWordConsumer | TNCqRNxC3epb6KAiVyUvfFHJEkN2miTTb1 |
Single Word Spec ID | 0x8495b310eb4a479f8982ad656521344900000000000000000000000000000000 |
Multi Word Spec ID | 0x1145310598fc4c25b825f4dae83e921e00000000000000000000000000000000 |
# How to use existing WINkLink AnyAPI
WARNING
Consumer contracts and jobs deployed are only provided as a sample for learning and testing. Users are advised to craft their own consumer contracts with bespoke jobs specifications.
# AnyAPI Request Process
Dapp/Consumer contract invokes a either single word/multi word request with the corresponding external job ID.
The Dapp contract calls the transferAndCall function of WinkMid to pay the required request price to the Operator. This method sends Wink tokens and executes the onTokenTransfer.
The onTokenTransfer logic of the Operator will trigger the oracle request method and emit OracleRequest event.
AnyAPI node subscribed to the chain will pick up this event and process it based on the external job ID.
Once operator contract receives callback response from the node, it will return the answer back to the Dapp/consumer contract.
To utilize WINkLink's operator contract and nodes users have to craft their own consumer contracts and job specifications and fund their own contracts for making requests.
# How to launch an AnyAPI Service Node
# Getting started
Maintainers for WINkLink need to understand how the TRON platform works, and know about smart contract deployment and the process of calling them. You're suggested to read related TRON official documents, particularly those on contract deployment on TronIDE.
Prepare the node account. You should read related Node account preparation doc.
# Required Environment
WINkLink node relies on a running PostgreSQL database. Developers can find more information in the official documentation postgresql official site (opens new window) .
TIP
Here we assume that the username and the password for the PostgreSQL instance deployed locally are root:root respectively. Please use a strong password or other verification methods in the production environment.
WINkLink node is written in Go programming language and requires Golang environment.
# Node Configuration
WINkLink node is configured using TOML files. Main config is tools/config/config.toml. With secrets.toml you can specify a db instance to be used. Below is a sample template for reference.
# secrets.toml
[Database]
URL = 'postgresql://root:root@localhost:5432/winklink?sslmode=disable' # Require
AllowSimplePasswords = true
[Password]
Keystore = 'keystorePassword' # Required
[Tron]
TronApiKey = 'apiKey'
After the node configuration file is confirmed, it is required to create password
and apicredentials
files and write the userid and password to access the node’s api:
# apicredentials
example.user@fake.email
totallyNotFakePassword (16 characters long)
# password
totallyNotFakePassword (16 characters long)
TIP
It is important that you keep private information safe.
# Building a docker image for the node
Use the following command to build a standard linux docker image:
# build a docker image
docker buildx build --platform linux/amd64 -t winklink-2.0 -f core/winklink.Dockerfile .
After building, we can tag and push it to the desired repository for deployment.
# Start a Node from source code
Install go1.20 (opens new window)
Go into the base directory of the source code winklink-2.0
Build the command line interface with
make install
Start your WINkLink node using the following command with the respective configuration items:
winklink -c /tools/config/config.toml -s /tools/config/secrets.toml node start -p /tools/secrets/password -a /tools/secrets/apicredentials
WARNING
Your node account must have enough TRX tokens for contract calls. You can apply testnet tokens at Testnet Faucet.
# Operator Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
import "./AuthorizedReceiver.sol";
import "./TRC20ReceiverInterface.sol";
import "./ConfirmedOwner.sol";
import "./TRC20Interface.sol";
import "./OperatorInterface.sol";
import "./OwnableInterface.sol";
import "./WithdrawalInterface.sol";
import "./SafeMathWinklink.sol";
/**
* @title The Winklink Operator contract
* @notice Node operators can deploy this contract to fulfill requests sent to them
*/
contract Operator is AuthorizedReceiver, ConfirmedOwner, TRC20ReceiverInterface, OperatorInterface, WithdrawalInterface {
using SafeMathWinklink for uint256;
struct Commitment {
bytes31 paramsHash;
uint8 dataVersion;
}
uint256 public constant getExpiryTime = 5 minutes;
uint256 private constant MAXIMUM_DATA_VERSION = 256;
uint256 private constant MINIMUM_CONSUMER_GAS_LIMIT = 400000;
uint256 private constant SELECTOR_LENGTH = 4;
uint256 private constant EXPECTED_REQUEST_WORDS = 2;
uint256 private constant MINIMUM_REQUEST_LENGTH = SELECTOR_LENGTH + (32 * EXPECTED_REQUEST_WORDS);
// We initialize fields to 1 instead of 0 so that the first invocation
// does not cost more gas.
uint256 private constant ONE_FOR_CONSISTENT_GAS_COST = 1;
// oracleRequest is intended for version 1, enabling single word responses
bytes4 private constant ORACLE_REQUEST_SELECTOR = this.oracleRequest.selector;
// operatorRequest is intended for version 2, enabling multi-word responses
bytes4 private constant OPERATOR_REQUEST_SELECTOR = this.operatorRequest.selector;
TRC20Interface internal immutable winkToken;
WinkMid internal immutable winkMid;
mapping(bytes32 => Commitment) private s_commitments;
mapping(address => bool) private s_owned;
// Tokens sent for requests that have not been fulfilled yet
uint256 private s_tokensInEscrow = ONE_FOR_CONSISTENT_GAS_COST;
event OracleRequest(
bytes32 indexed specId,
address requester,
bytes32 requestId,
uint256 payment,
address callbackAddr,
bytes4 callbackFunctionId,
uint256 cancelExpiration,
uint256 dataVersion,
bytes data
);
event CancelOracleRequest(bytes32 indexed requestId);
event OracleResponse(bytes32 indexed requestId);
event OwnableContractAccepted(address indexed acceptedContract);
event TargetsUpdatedAuthorizedSenders(address[] targets, address[] senders, address changedBy);
/**
* @notice Deploy with the address of the WINK token
* @dev Sets the WinkToken address for the imported TRC20Interface
* @param wink The address of the WINK token
* @param owner The address of the owner
*/
constructor(address wink, address _winkMid, address owner) ConfirmedOwner(owner) {
winkToken = TRC20Interface(wink); // external but already deployed and unalterable
winkMid = WinkMid(_winkMid);
}
/**
* @notice The type and version of this contract
* @return Type and version string
*/
function typeAndVersion() external pure virtual returns (string memory) {
return "Operator 1.0.0";
}
/**
* @notice Creates the Winklink request. This is a backwards compatible API
* with the Oracle.sol contract, but the behavior changes because
* callbackAddress is assumed to be the same as the request sender.
* @param callbackAddress The consumer of the request
* @param payment The amount of payment given (specified in wei)
* @param specId The Job Specification ID
* @param callbackAddress The address the oracle data will be sent to
* @param callbackFunctionId The callback function ID for the response
* @param nonce The nonce sent by the requester
* @param dataVersion The specified data version
* @param data The extra request parameters
*/
function oracleRequest(
address sender,
uint256 payment,
bytes32 specId,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 nonce,
uint256 dataVersion,
bytes calldata data
) external override {
(bytes32 requestId, uint256 expiration) = _verifyAndProcessOracleRequest(
sender,
payment,
callbackAddress,
callbackFunctionId,
nonce,
dataVersion
);
emit OracleRequest(specId, sender, requestId, payment, sender, callbackFunctionId, expiration, dataVersion, data);
}
/**
* @notice Creates the Winklink request
* @dev Stores the hash of the params as the on-chain commitment for the request.
* Emits OracleRequest event for the Winklink node to detect.
* @param sender The sender of the request
* @param payment The amount of payment given (specified in wei)
* @param specId The Job Specification ID
* @param callbackFunctionId The callback function ID for the response
* @param nonce The nonce sent by the requester
* @param dataVersion The specified data version
* @param data The extra request parameters
*/
function operatorRequest(
address sender,
uint256 payment,
bytes32 specId,
bytes4 callbackFunctionId,
uint256 nonce,
uint256 dataVersion,
bytes calldata data
) external override {
(bytes32 requestId, uint256 expiration) = _verifyAndProcessOracleRequest(
sender,
payment,
sender,
callbackFunctionId,
nonce,
dataVersion
);
emit OracleRequest(specId, sender, requestId, payment, sender, callbackFunctionId, expiration, dataVersion, data);
}
/**
* @notice Called by the Winklink node to fulfill requests
* @dev Given params must hash back to the commitment stored from `oracleRequest`.
* Will call the callback address' callback function without bubbling up error
* checking in a `require` so that the node can get paid.
* @param requestId The fulfillment request ID that must match the requesters'
* @param payment The payment amount that will be released for the oracle (specified in wei)
* @param callbackAddress The callback address to call for fulfillment
* @param callbackFunctionId The callback function ID to use for fulfillment
* @param expiration The expiration that the node should respond by before the requester can cancel
* @param data The data to return to the consuming contract
* @return Status if the external call was successful
*/
function fulfillOracleRequest(
bytes32 requestId,
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 expiration,
bytes32 data
)
external
override
validateAuthorizedSender
validateRequestId(requestId)
validateCallbackAddress(callbackAddress)
returns (bool)
{
_verifyOracleRequestAndProcessPayment(requestId, payment, callbackAddress, callbackFunctionId, expiration, 1);
emit OracleResponse(requestId);
// require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas");
// All updates to the oracle's fulfillment should come before calling the
// callback(addr+functionId) as it is untrusted.
// See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern
(bool success, ) = callbackAddress.call(abi.encodeWithSelector(callbackFunctionId, requestId, data)); // solhint-disable-line avoid-low-level-calls
return success;
}
/**
* @notice Called by the Winklink node to fulfill requests with multi-word support
* @dev Given params must hash back to the commitment stored from `oracleRequest`.
* Will call the callback address' callback function without bubbling up error
* checking in a `require` so that the node can get paid.
* @param requestId The fulfillment request ID that must match the requester's
* @param payment The payment amount that will be released for the oracle (specified in wei)
* @param callbackAddress The callback address to call for fulfillment
* @param callbackFunctionId The callback function ID to use for fulfillment
* @param expiration The expiration that the node should respond by before the requester can cancel
* @param data The data to return to the consuming contract
* @return Status if the external call was successful
*/
function fulfillOracleRequest2(
bytes32 requestId,
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 expiration,
bytes calldata data
)
external
override
validateAuthorizedSender
validateRequestId(requestId)
validateCallbackAddress(callbackAddress)
validateMultiWordResponseId(requestId, data)
returns (bool)
{
_verifyOracleRequestAndProcessPayment(requestId, payment, callbackAddress, callbackFunctionId, expiration, 2);
emit OracleResponse(requestId);
// require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas");
// All updates to the oracle's fulfillment should come before calling the
// callback(addr+functionId) as it is untrusted.
// See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern
(bool success, ) = callbackAddress.call(abi.encodePacked(callbackFunctionId, data)); // solhint-disable-line avoid-low-level-calls
return success;
}
/**
* @notice Transfer the ownership of ownable contracts. This is primarily
* intended for Authorized Forwarders but could possibly be extended to work
* with future contracts.
* @param ownable list of addresses to transfer
* @param newOwner address to transfer ownership to
*/
function transferOwnableContracts(address[] calldata ownable, address newOwner) external onlyOwner {
for (uint256 i = 0; i < ownable.length; i++) {
s_owned[ownable[i]] = false;
OwnableInterface(ownable[i]).transferOwnership(newOwner);
}
}
/**
* @notice Accept the ownership of an ownable contract. This is primarily
* intended for Authorized Forwarders but could possibly be extended to work
* with future contracts.
* @dev Must be the pending owner on the contract
* @param ownable list of addresses of Ownable contracts to accept
*/
function acceptOwnableContracts(address[] calldata ownable) public validateAuthorizedSenderSetter {
for (uint256 i = 0; i < ownable.length; i++) {
s_owned[ownable[i]] = true;
emit OwnableContractAccepted(ownable[i]);
OwnableInterface(ownable[i]).acceptOwnership();
}
}
/**
* @notice Sets the fulfillment permission for
* @param targets The addresses to set permissions on
* @param senders The addresses that are allowed to send updates
*/
function setAuthorizedSendersOn(address[] calldata targets, address[] calldata senders)
public
validateAuthorizedSenderSetter
{
TargetsUpdatedAuthorizedSenders(targets, senders, msg.sender);
for (uint256 i = 0; i < targets.length; i++) {
AuthorizedReceiverInterface(targets[i]).setAuthorizedSenders(senders);
}
}
/**
* @notice Accepts ownership of ownable contracts and then immediately sets
* the authorized sender list on each of the newly owned contracts. This is
* primarily intended for Authorized Forwarders but could possibly be
* extended to work with future contracts.
* @param targets The addresses to set permissions on
* @param senders The addresses that are allowed to send updates
*/
function acceptAuthorizedReceivers(address[] calldata targets, address[] calldata senders)
external
validateAuthorizedSenderSetter
{
acceptOwnableContracts(targets);
setAuthorizedSendersOn(targets, senders);
}
/**
* @notice Allows the node operator to withdraw earned WINK to a given address
* @dev The owner of the contract can be another wallet and does not have to be a Winklink node
* @param recipient The address to send the WINK token to
* @param amount The amount to send (specified in wei)
*/
function withdraw(address recipient, uint256 amount)
external
override(OracleInterface, WithdrawalInterface)
onlyOwner
validateAvailableFunds(amount)
{
assert(winkToken.transfer(recipient, amount));
}
/**
* @notice Displays the amount of WINK that is available for the node operator to withdraw
* @dev We use `ONE_FOR_CONSISTENT_GAS_COST` in place of 0 in storage
* @return The amount of withdrawable WINK on the contract
*/
function withdrawable() external view override(OracleInterface, WithdrawalInterface) returns (uint256) {
return _fundsAvailable();
}
/**
* @notice Forward a call to another contract
* @dev Only callable by the owner
* @param to address
* @param data to forward
*/
function ownerForward(address to, bytes calldata data) external onlyOwner validateNotToWINK(to) {
require(isContract(to), "Must forward to a contract");
(bool status, ) = to.call(data);
require(status, "Forwarded call failed");
}
/**
* @notice Interact with other WINKTokenReceiver contracts by calling transferAndCall
* @param to The address to transfer to.
* @param value The amount to be transferred.
* @param data The extra data to be passed to the receiving contract.
* @return success bool
*/
function ownerTransferAndCall(
address to,
uint64 value,
bytes calldata data
) external override onlyOwner validateAvailableFunds(value) returns (bool success) {
winkToken.approve(address(winkMid), value);
return winkMid.transferAndCall(to, value, data);
}
/**
* @notice Distribute funds to multiple addresses using ETH send
* to this payable function.
* @dev Array length must be equal, TRX sent must equal the sum of amounts.
* A malicious receiver could cause the distribution to revert, in which case
* it is expected that the address is removed from the list.
* @param receivers list of addresses
* @param amounts list of amounts
*/
function distributeFunds(address payable[] calldata receivers, uint256[] calldata amounts) external payable {
require(receivers.length > 0 && receivers.length == amounts.length, "Invalid array length(s)");
uint256 valueRemaining = msg.value;
for (uint256 i = 0; i < receivers.length; i++) {
uint256 sendAmount = amounts[i];
valueRemaining = valueRemaining.sub(sendAmount);
receivers[i].transfer(sendAmount);
}
require(valueRemaining == 0, "Too much TRX sent");
}
/**
* @notice Allows recipient to cancel requests sent to this oracle contract.
* Will transfer the WINK sent for the request back to the recipient address.
* @dev Given params must hash to a commitment stored on the contract in order
* for the request to be valid. Emits CancelOracleRequest event.
* @param requestId The request ID
* @param payment The amount of payment given (specified in wei)
* @param callbackFunc The requester's specified callback function selector
* @param expiration The time of the expiration for the request
*/
function cancelOracleRequest(
bytes32 requestId,
uint64 payment,
bytes4 callbackFunc,
uint256 expiration
) external override {
bytes31 paramsHash = _buildParamsHash(payment, msg.sender, callbackFunc, expiration);
require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID");
// solhint-disable-next-line not-rely-on-time
require(expiration <= block.timestamp, "Request is not expired");
delete s_commitments[requestId];
emit CancelOracleRequest(requestId);
winkToken.transfer(msg.sender, payment);
}
/**
* @notice Allows requester to cancel requests sent to this oracle contract.
* Will transfer the WINK sent for the request back to the recipient address.
* @dev Given params must hash to a commitment stored on the contract in order
* for the request to be valid. Emits CancelOracleRequest event.
* @param nonce The nonce used to generate the request ID
* @param payment The amount of payment given (specified in wei)
* @param callbackFunc The requester's specified callback function selector
* @param expiration The time of the expiration for the request
*/
function cancelOracleRequestByRequester(
uint256 nonce,
uint256 payment,
bytes4 callbackFunc,
uint256 expiration
) external {
bytes32 requestId = keccak256(abi.encodePacked(msg.sender, nonce));
bytes31 paramsHash = _buildParamsHash(payment, msg.sender, callbackFunc, expiration);
require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID");
// solhint-disable-next-line not-rely-on-time
require(expiration <= block.timestamp, "Request is not expired");
delete s_commitments[requestId];
emit CancelOracleRequest(requestId);
winkToken.transfer(msg.sender, payment);
}
/**
* @notice Returns the address of the WINK token
* @dev This is the public implementation for WinklinkTokenAddress, which is
* an internal method of the WinklinkClient contract
*/
function getWinklinkToken() public view returns (address) {
return address(winkMid);
}
/**
* @notice Require that the token transfer action is valid
* @dev OPERATOR_REQUEST_SELECTOR = multiword, ORACLE_REQUEST_SELECTOR = singleword
*/
function _validateTokenTransferAction(bytes4 funcSelector, bytes memory data) internal pure {
require(data.length >= MINIMUM_REQUEST_LENGTH, "Invalid request length");
require(
funcSelector == OPERATOR_REQUEST_SELECTOR || funcSelector == ORACLE_REQUEST_SELECTOR,
"Must use whitelisted functions"
);
}
/**
* @notice Verify the Oracle Request and record necessary information
* @param sender The sender of the request
* @param payment The amount of payment given (specified in wei)
* @param callbackAddress The callback address for the response
* @param callbackFunctionId The callback function ID for the response
* @param nonce The nonce sent by the requester
*/
function _verifyAndProcessOracleRequest(
address sender,
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 nonce,
uint256 dataVersion
) private validateNotToWINK(callbackAddress) returns (bytes32 requestId, uint256 expiration) {
requestId = keccak256(abi.encodePacked(sender, nonce));
require(s_commitments[requestId].paramsHash == 0, "Must use a unique ID");
// solhint-disable-next-line not-rely-on-time
expiration = block.timestamp.add(getExpiryTime);
bytes31 paramsHash = _buildParamsHash(payment, callbackAddress, callbackFunctionId, expiration);
s_commitments[requestId] = Commitment(paramsHash, _safeCastToUint8(dataVersion));
s_tokensInEscrow = s_tokensInEscrow.add(payment);
return (requestId, expiration);
}
/**
* @notice Verify the Oracle request and unlock escrowed payment
* @param requestId The fulfillment request ID that must match the requester's
* @param payment The payment amount that will be released for the oracle (specified in wei)
* @param callbackAddress The callback address to call for fulfillment
* @param callbackFunctionId The callback function ID to use for fulfillment
* @param expiration The expiration that the node should respond by before the requester can cancel
*/
function _verifyOracleRequestAndProcessPayment(
bytes32 requestId,
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 expiration,
uint256 dataVersion
) internal {
bytes31 paramsHash = _buildParamsHash(payment, callbackAddress, callbackFunctionId, expiration);
require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID");
require(s_commitments[requestId].dataVersion <= _safeCastToUint8(dataVersion), "Data versions must match");
s_tokensInEscrow = s_tokensInEscrow.sub(payment);
delete s_commitments[requestId];
}
/**
* @notice Build the bytes31 hash from the payment, callback and expiration.
* @param payment The payment amount that will be released for the oracle (specified in wei)
* @param callbackAddress The callback address to call for fulfillment
* @param callbackFunctionId The callback function ID to use for fulfillment
* @param expiration The expiration that the node should respond by before the requester can cancel
* @return hash bytes31
*/
function _buildParamsHash(
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 expiration
) internal pure returns (bytes31) {
return bytes31(keccak256(abi.encodePacked(payment, callbackAddress, callbackFunctionId, expiration)));
}
/**
* @notice Safely cast uint256 to uint8
* @param number uint256
* @return uint8 number
*/
function _safeCastToUint8(uint256 number) internal pure returns (uint8) {
require(number < MAXIMUM_DATA_VERSION, "number too big to cast");
return uint8(number);
}
/**
* @notice Returns the WINK available in this contract, not locked in escrow
* @return uint256 WINK tokens available
*/
function _fundsAvailable() private view returns (uint256) {
uint256 inEscrow = s_tokensInEscrow.sub(ONE_FOR_CONSISTENT_GAS_COST);
return winkToken.balanceOf(address(this)).sub(inEscrow);
}
/**
* @notice concrete implementation of AuthorizedReceiver
* @return bool of whether sender is authorized
*/
function _canSetAuthorizedSenders() internal view override returns (bool) {
return isAuthorizedSender(msg.sender) || owner() == msg.sender;
}
function isContract(address _addr) private view returns (bool hasCode)
{
uint length;
assembly { length := extcodesize(_addr) }
return length > 0;
}
function onTokenTransfer(
address sender,
uint64 amount,
bytes memory data
) public override {
assembly {
// solhint-disable-next-line avoid-low-level-calls
mstore(add(data, 36), sender) // ensure correct sender is passed
// solhint-disable-next-line avoid-low-level-calls
mstore(add(data, 68), amount) // ensure correct amount is passed
}
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(this).delegatecall(data); // calls oracleRequest
require(success, "Unable to create request");
}
// MODIFIERS
/**
* @dev Reverts if the first 32 bytes of the bytes array is not equal to requestId
* @param requestId bytes32
* @param data bytes
*/
modifier validateMultiWordResponseId(bytes32 requestId, bytes calldata data) {
require(data.length >= 32, "Response must be > 32 bytes");
bytes32 firstDataWord;
assembly {
firstDataWord := calldataload(data.offset)
}
require(requestId == firstDataWord, "First word must be requestId");
_;
}
/**
* @dev Reverts if amount requested is greater than withdrawable balance
* @param amount The given amount to compare to `s_withdrawableTokens`
*/
modifier validateAvailableFunds(uint256 amount) {
require(_fundsAvailable() >= amount, "Amount requested is greater than withdrawable balance");
_;
}
/**
* @dev Reverts if request ID does not exist
* @param requestId The given request ID to check in stored `commitments`
*/
modifier validateRequestId(bytes32 requestId) {
require(s_commitments[requestId].paramsHash != 0, "Must have a valid requestId");
_;
}
/**
* @dev Reverts if the callback address is the WINK token
* @param to The callback address
*/
modifier validateNotToWINK(address to) {
require(to != address(winkToken), "Cannot call to WINK");
_;
}
/**
* @dev Reverts if the target address is owned by the operator
*/
modifier validateCallbackAddress(address callbackAddress) {
require(!s_owned[callbackAddress], "Cannot call owned contract");
_;
}
}
# How to setup AnyAPI contracts and jobs
# WinkMid Contract
WINkLink uses WIN (TRC20) as the base token for the whole platform.
WINkLink adopts the transferAndCall
feature, i.e. calling one of the callback functions while transferring TRC20
tokens to contracts, a feature similar to ERC677
yet adopting different interface parameters.
Given that we cannot modify contracts or add interfaces for most of the tokens issued, WINkLink provides WinkMid wrapper contract, which helps wrapping any TRC20 token and provides transferAndCall interface.
The contract code is available at WinkMid.sol
.
For convenience, Nile TestNet has deployed WinkMid contract and encapsulated the WIN token on it. Developers may use this contract address directly without additional deployment. Users may also claim test TRX and WIN tokens from the Faucet address provided by Nile TestNet.
TIP
Nile Testnet
WIN TRC20 Contract Address: TNDSHKGBmgRx9mDYA9CnxPx55nu672yQw2
WinkMid Contract Address: TLLEKGqhH4MiN541BDaGpXD7MRkwG2mTro
Testnet Faucet: https://nileex.io/join/getJoinPage (opens new window)
When deploying WinkMid contract, developers need to provide the encapsulated TRC20
token address (i.e. WIN token address) for the constructor.
Developers do not need to call WinkMid contract directly, as it's a wink helper for caller contracts.
WIN token address and WinkMid contract address are needed in the constructor function when deploying an Coordinator contract.
# Operator Contract
The operator contract is main contract to handle all requests from the consumer contract and fulfillment from the WINkLink node. Deploy with the respective arguments.
After deploying the operator contract, Oracle needs to be approved for fulfillment by adding it to the list using setAuthorizedSender method.
# Consumer Contract
In the example below, we have 2 main types of consumer contracts
- SingleWordConsumer
Returns a single value from a single request.
- MultiWordConsumer
Returns multi values from a single request.
Either of the contracts can be deployed with the respective arguments based on the user's requirements.
WARNING
Set Spec ID in the consumer contract to the corresponding external job ID after removing '-' and right pad zeroes in bytes32
e.g. 0x8495b310eb4a479f8982ad656521344900000000000000000000000000000000
# Single word request
This example provides a sample user contract that request for a single word response.
The contract takes in a user entered URL with the desired path to the data. The configured job for this example retrieves either USD, EUR or SGD prices for TRX.
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./WinklinkClient.sol";
contract SingleWordConsumer is WinklinkClient {
using Winklink for Winklink.Request;
bytes32 internal specId;
bytes32 public currentPrice;
event RequestFulfilled(
bytes32 indexed requestId, // User-defined ID
bytes32 indexed price
);
constructor(
address _wink,
address _winkMid,
address _oracle,
bytes32 _specId
) public {
setWinklinkToken(_wink);
setWinkMid(_winkMid);
setWinklinkOracle(_oracle);
specId = _specId;
}
function setSpecID(bytes32 _specId) public {
specId = _specId;
}
//https://min-api.cryptocompare.com/data/price?fsym=TRX&tsyms=USD,EUR,SGD
function requestTRXPrice(string memory _urlPriceCompare, string memory _currency, uint64 _payment) public {
Winklink.Request memory req = buildOperatorRequest(specId, this.fulfill.selector);
req.add("get", _urlPriceCompare);
string[] memory path = new string[](1);
path[0] = _currency;
req.addStringArray("path", path);
// version 2
sendWinklinkRequest(req, _payment);
}
function cancelRequest(
address _oracle,
bytes32 _requestId,
uint64 _payment,
bytes4 _callbackFunctionId,
uint256 _expiration
) public {
WinklinkRequestInterface requested = WinklinkRequestInterface(_oracle);
requested.cancelOracleRequest(_requestId, _payment, _callbackFunctionId, _expiration);
}
function withdrawWink() public {
TRC20Interface _wink = TRC20Interface(WinklinkTokenAddress());
require(_wink.transfer(msg.sender, _wink.balanceOf(address(this))), "Unable to transfer");
}
function addExternalRequest(address _oracle, bytes32 _requestId) external {
addWinklinkExternalRequest(_oracle, _requestId);
}
function fulfill(bytes32 _requestId, bytes32 _price) public recordWinklinkFulfillment(_requestId) {
emit RequestFulfilled(_requestId, _price);
currentPrice = _price;
}
}
Within WinkLink Node, we need to setup a specific job to handle and do the required
type = "directrequest"
schemaVersion = 1
name = "DR: SingleWordRequest"
externalJobID = "8495b310-eb4a-479f-8982-ad6565213449"
forwardingAllowed = false
maxTaskDuration = "0s"
contractAddress = "0x51d389Ce2ba948C9c2fc382Efc910B5776D50E4b"
tvmChainID = "2"
minContractPaymentWin = "5"
requesters = [ "0xcFbe00786F9dC12d5985f1cD64657384F6065CD2" ]
fromAddress = "0x40544c785F4127f39c9Ad3321BE4f439eE8bd73C"
observationSource = """
decode_log [type=tvmabidecodelog 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)"]
ds1 [type=http method=GET url="$(decode_cbor.get)" allowunrestrictednetworkaccess="true"]
ds1_parse [type=jsonparse path="$(decode_cbor.path)"]
ds1_multiply [type=multiply value="$(ds1_parse)" times=100]
encode_data [type=tvmabiencode abi="(uint256 value)" data=<{"value": $(ds1_multiply)}>]
encode_tx [type=tvmabiencode abi="fulfillOracleRequest(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes32 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 [type=tvmcall contract="THRs9Y3vqE4FMTE7LPjMB4LFEH8uUsZaE4" method="fulfillOracleRequest(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes32 data)" data="$(encode_tx)" extractRevertReason=true]
decode_log->decode_cbor->ds1 -> ds1_parse -> ds1_multiply->encode_data->encode_tx->submit
"""
The single word job specification does the following:
- Decode the event message retrieved from the chain
- Perform http operation using the given URL
- Retrieve the data from the given path using the path provided
- Apply any required transformations to the data
- Encode data into a response and submit to operator
The operator contract will receive the response and forward it back to the user contract.
# Multi word request
This example provides a sample user contract for multi word request.
The contract takes in a user entered URL with all desired paths to the data.
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.0;
import "./WinklinkClient.sol";
contract MultiWordConsumer is WinklinkClient {
using Winklink for Winklink.Request;
bytes32 internal specId;
uint256 public currentUSDPriceInt;
uint256 public currentEURPriceInt;
uint256 public currentJPYPriceInt;
event RequestFulfilled2(
bytes32 indexed requestId,
bytes32 usd,
bytes32 eur,
bytes32 jpy
);
constructor(
address _wink,
address _winkMid,
address _oracle,
bytes32 _specId
) public {
setWinklinkToken(_wink);
setWinkMid(_winkMid);
setWinklinkOracle(_oracle);
specId = _specId;
}
function setSpecID(bytes32 _specId) public {
specId = _specId;
}
//https://min-api.cryptocompare.com/data/price?fsym=TRX&tsyms=USD,EUR,JPY
function requestMultipleParametersWithCustomURLs(
string memory _urlPriceCompare,
string memory _pathUSD,
string memory _pathEUR,
string memory _pathJPY,
uint64 _payment
) public {
Winklink.Request memory req = buildOperatorRequest(specId, this.fulfillParametersWithCustomURLs.selector);
req.add("urlPriceCompare", _urlPriceCompare);
req.add("pathUSD", _pathUSD);
req.add("pathEUR", _pathEUR);
req.add("pathJPY", _pathJPY);
sendWinklinkRequest(req, _payment);
}
function cancelRequest(
address _oracle,
bytes32 _requestId,
uint64 _payment,
bytes4 _callbackFunctionId,
uint256 _expiration
) public {
WinklinkRequestInterface requested = WinklinkRequestInterface(_oracle);
requested.cancelOracleRequest(_requestId, _payment, _callbackFunctionId, _expiration);
}
function withdrawWink() public {
TRC20Interface _wink = TRC20Interface(WinklinkTokenAddress());
require(_wink.transfer(msg.sender, _wink.balanceOf(address(this))), "Unable to transfer");
}
function addExternalRequest(address _oracle, bytes32 _requestId) external {
addWinklinkExternalRequest(_oracle, _requestId);
}
function fulfillParametersWithCustomURLs(bytes32 _requestId, uint256 _usd, uint256 _eur, uint _jpy)
public
recordWinklinkFulfillment(_requestId)
{
emit RequestFulfilled2(_requestId, bytes32(_usd), bytes32(_eur), bytes32(_jpy));
currentUSDPriceInt = _usd;
currentEURPriceInt = _eur;
currentJPYPriceInt = _jpy;
}
}
In the node, we add the job specification to support getting all the data and respective transformations
type = "directrequest"
schemaVersion = 1
name = "DR: MultiWordRequest"
externalJobID = "11453105-98fc-4c25-b825-f4dae83e921e"
forwardingAllowed = false
maxTaskDuration = "0s"
contractAddress = "0x51d389Ce2ba948C9c2fc382Efc910B5776D50E4b"
tvmChainID = "2"
minContractPaymentWin = "1"
requesters = [ "0xcFbe00786F9dC12d5985f1cD64657384F6065CD2" ]
fromAddress = "0x40544c785F4127f39c9Ad3321BE4f439eE8bd73C"
observationSource = """
decode_log [type=tvmabidecodelog
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)"]
decode_log -> decode_cbor
decode_cbor -> usd
decode_cbor -> eur
decode_cbor -> jpy
usd [type=http method=GET url="$(decode_cbor.urlPriceCompare)" allowunrestrictednetworkaccess="true"]
usd_parse [type=jsonparse path="$(decode_cbor.pathUSD)"]
usd_multiply [type=multiply value="$(usd_parse)", times="100"]
usd -> usd_parse -> usd_multiply
eur [type=http method=GET url="$(decode_cbor.urlPriceCompare)" allowunrestrictednetworkaccess="true"]
eur_parse [type=jsonparse path="$(decode_cbor.pathEUR)"]
eur_multiply [type=multiply value="$(eur_parse)", times="100"]
eur -> eur_parse -> eur_multiply
jpy [type=http method=GET url="$(decode_cbor.urlPriceCompare)" allowunrestrictednetworkaccess="true"]
jpy_parse [type=jsonparse path="$(decode_cbor.pathJPY)"]
jpy_multiply [type=multiply value="$(jpy_parse)", times="100000"]
jpy -> jpy_parse -> jpy_multiply
usd_multiply -> encode_mwr
eur_multiply -> encode_mwr
jpy_multiply -> encode_mwr
encode_mwr [type=tvmabiencode
abi="(bytes32 requestId, uint256 usd, uint256 eur, uint256 jpy)"
data=<{
"requestId": $(decode_log.requestId),
"usd": $(usd_multiply),
"eur": $(eur_multiply),
"jpy": $(jpy_multiply)}>]
encode_tx [type=tvmabiencode
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_mwr)}>]
submit_tx [type=tvmcall contract ="THRs9Y3vqE4FMTE7LPjMB4LFEH8uUsZaE4" data="$(encode_tx)" method="fulfillOracleRequest2(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes data)" extractRevertReason=true]
encode_mwr -> encode_tx -> submit_tx
"""
The multi word job specification does the following:
- Decode the event message retrieved from the chain
- Perform http operation using the given URL
- Retrieve the data from the given path using the path provided
- Apply any required transformations to the data
Steps 3 & 4 are repeated for as many times as needed for the number of data it needs to obtain.
- Encode data into a response and submit to operator
This job specification is mostly similar to the single word above with the exception of steps 3 and 4 which consists of data retrieval and transformation which is required for all the data.
# Internal Adapters
This section shows the available internal adapters that can be used to craft job specifications to obtain the desired data.
Task | Description | Input Type | Output Type |
---|---|---|---|
tvm abi decode log | Decode events retrieved from the chain | []byte | map[string] |
tvm call | Perform call to contracts on tvm chain | []byte | Contract call Return (opens new window) struct |
hex decode | Decode hexadecimal into string | string | string |
hex encode | Encode string into hexadecimal | string/[]byte/decimal/big.Int | string |
base64 decode | Decode string to base64 | string | []byte |
base64 encode | Encode base64 to string | string/[]byte | string |
http | Perform HTTP call | string (method) url (url) map[string] (requestData) bool (allowUnrestrictedNetworkAccess) []string (reqHeaders) | string |
json parse | Obtain value from json | string (Path) string (Separator) string (Data) | map[string]interface{}/[]interface{} |
length | Obtain length of string | string | decimal |
less than | Check whether input is less than limit | string (input) string (limit) | bool |
lower case | Convert string to lower case | string | string |
upper case | Convert string to upper case | string | string |
any | Obtain any value within the input | decimal/string | string |
mean | Obtain the mean value given inputs | string (Values) string (AllowedFaults) string (Precision) | decimal |
median | Obtain the median value given inputs | string (Values) string (AllowedFaults) | decimal |
memo | Return input value | string | string |
merge | Merge two string inputs | string (left) string (right) | map[string]interface{} |
multiply | Multiply two input values | string (Input) string (Times) | decimal |
divide | Divide input by divisor | string (Input) string (Divisor) string (Precision) | decimal |
sum | Sum of two input values | string (Values) string (AllowedFaults) | decimal |
cbor parse | Cbor decodes the input | string (Data) string (Mode) | interface{} |