上一章节我们已经,部署了Truora,并且访问正常,这一章节我们开始编写我们的核心的合约,并部署连接上Truora服务。
前言
上一章节我们已经,部署了Truora,并且访问正常,这一章节我们开始编写我们的核心的合约,并部署连接上Truora服务。
编写预言机合约
这里我们可以参照官方的步骤:https://truora.readthedocs.io/zh_CN/latest/docs/develop/quick-start.html#id1
1.创建用户
打开一键部署的 WeBASE-Front 页面,默认:http://{IP}:5002/WeBASE-Front/,使用部署主机的 IP 地址替换 {IP}。 点击左边 合约管理 –> 测试用户,创建一个调试用户 larry
2.上传核心预言机合约
点击左边 合约管理 –> 合约 IDE,选择 solidity 版本,上传模板合约,包括以下五个合约:
FiscoOracleClient.sol
pragma solidity ^0.6.0; import "./SafeMath.sol"; import "./OracleCoreInterface.sol"; abstract contract FiscoOracleClient { using SafeMath for uint256; OracleCoreInterface private oracle; uint256 private requestCount = 1; mapping(bytes32 => address) private pendingRequests; mapping (address => uint) private reqc; uint256 constant public EXPIRY_TIME = 10 * 60 * 1000; event Requested(bytes32 indexed id); event Fulfilled(bytes32 indexed id); function __callback(bytes32 requestId, int256 result) public virtual; // __callback with proof // function __callback(bytes32 requestId, int256 result, bytes calldata proof) public virtual; function oracleQuery(address _oracle, string memory url, uint256 timesAmount) internal returns (bytes32 requestId) { return oracleQuery(EXPIRY_TIME,"url", _oracle, url, timesAmount, false); } function oracleQuery(uint expiryTime, string memory datasource, address _oracle, string memory url, uint256 timesAmount, bool needProof) internal returns (bytes32 requestId) { // calculate the id; oracle = OracleCoreInterface(_oracle); int256 chainId; int256 groupId; ( chainId, groupId) = oracle.getChainIdAndGroupId(); requestId = keccak256(abi.encodePacked(chainId, groupId, this, requestCount)); pendingRequests[requestId] = _oracle; emit Requested(requestId); require(oracle.query(address(this),requestCount, url,timesAmount, expiryTime,needProof),"oracle-core invoke failed!"); requestCount++; reqc[msg.sender]++; return requestId; } /** * @notice Sets the stored oracle core address * @param _oracle The address of the oracle core contract */ function setOracleCoreAddress(address _oracle) internal { oracle = OracleCoreInterface(_oracle); } /** * @notice Retrieves the stored address of the oracle contract * @return The address of the oracle contract */ function getOracleCoreAddress() internal view returns (address) { return address(oracle); } /** * @dev Reverts if the sender is not the oracle of the request. * @param _requestId The request ID for fulfillment */ modifier onlyOracleCoreInvoke(bytes32 _requestId) { require(msg.sender == pendingRequests[_requestId], "Source must be the oracle of the request"); delete pendingRequests[_requestId]; emit Fulfilled(_requestId); _; } }
OracleCore.sol
pragma solidity ^0.6.0; import "./Ownable.sol"; import "./SafeMath.sol"; /** * @title The contract for oracle service listening */ contract OracleCore is Ownable { using SafeMath for uint256; mapping(bytes32 => bytes32) private commitments; mapping(bytes32 => uint256) timeoutMap; int256 private chainId; int256 private groupId; bytes4 private callbackFunctionId = bytes4(keccak256("__callback(bytes32,int256)")); event OracleRequest( address callbackAddr, bytes32 requestId, string url, uint256 expiration, uint256 timesAmount, bool needProof ); constructor(int256 _chainId, int256 _groupId) public Ownable() { chainId = _chainId; groupId = _groupId; } function query( address _callbackAddress, uint256 _nonce, string calldata _url, uint256 _timesAmount, uint256 _expiryTime, bool _needProof ) external returns(bool) { bytes32 requestId = keccak256(abi.encodePacked(chainId, groupId, _callbackAddress, _nonce)); require(commitments[requestId] == 0, "Must use a unique ID"); uint256 expiration = now.add(_expiryTime); timeoutMap[requestId] = expiration; commitments[requestId] = keccak256( abi.encodePacked( _callbackAddress, expiration ) ); emit OracleRequest( _callbackAddress, requestId, _url, expiration, _timesAmount, _needProof); return true; } function fulfillRequest( bytes32 _requestId, address _callbackAddress, uint256 _expiration, uint256 _result, bytes calldata proof ) public onlyOwner isValidRequest(_requestId) returns (bool) { bytes32 paramsHash = keccak256( abi.encodePacked( _callbackAddress, _expiration ) ); require(commitments[_requestId] == paramsHash, "Params do not match request ID"); delete commitments[_requestId]; delete timeoutMap[_requestId]; (bool success, ) = _callbackAddress.call(abi.encodeWithSelector(callbackFunctionId, _requestId, _result)); // solhint-disable-line avoid-low-level-calls return success; } function getChainIdAndGroupId() public view returns(int256,int256){ return (chainId, groupId); } /** * @dev Reverts if request ID does not exist or time out. * @param _requestId The given request ID to check in stored `commitments` */ modifier isValidRequest(bytes32 _requestId) { require(commitments[_requestId] != 0, "Must have a valid requestId"); require(timeoutMap[_requestId] > now, "fulfill request time out"); _; } }
OracleCoreInterface.sol
pragma solidity ^0.6.0; interface OracleCoreInterface { function query( address _callbackAddress, uint256 _nonce, string calldata _url, uint256 _timesAmount, uint256 _expiryTime, bool needProof ) external returns(bool) ; function getChainIdAndGroupId() external view returns(int256,int256) ; }
Ownable.sol
pragma solidity ^0.6.0; /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be aplied to your functions to restrict their use to * the owner. * * This contract has been modified to remove the revokeOwnership function */ contract Ownable { address private _owner; event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the deployer as the initial owner. */ constructor () internal { _owner = msg.sender; emit OwnershipTransferred(address(0), _owner); } /** * @dev Returns the address of the current owner. */ function owner() public view returns (address) { return _owner; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(isOwner(), "Ownable: caller is not the owner"); _; } /** * @dev Returns true if the caller is the current owner. */ function isOwner() public view returns (bool) { return msg.sender == _owner; } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public onlyOwner { _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). */ function _transferOwnership(address newOwner) internal { require(newOwner != address(0), "Ownable: new owner is the zero address"); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } }
SafeMath.sol
pragma solidity ^0.6.0; /** * @dev Wrappers over Solidity's arithmetic operations with added overflow * checks. * * Arithmetic operations in Solidity wrap on overflow. This can easily result * in bugs, because programmers usually assume that an overflow raises an * error, which is the standard behavior in high level programming languages. * `SafeMath` restores this intuition by reverting the transaction when an * operation overflows. * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. */ library SafeMath { /** * @dev Returns the addition of two unsigned integers, reverting on * overflow. * * Counterpart to Solidity's `+` operator. * * Requirements: * - Addition cannot overflow. */ function add(uint256 a, uint256 b) internal pure returns (uint256) { uint256 c = a + b; require(c >= a, "SafeMath: addition overflow"); return c; } /** * @dev Returns the subtraction of two unsigned integers, reverting on * overflow (when the result is negative). * * Counterpart to Solidity's `-` operator. * * Requirements: * - Subtraction cannot overflow. */ function sub(uint256 a, uint256 b) internal pure returns (uint256) { require(b <= a, "SafeMath: subtraction overflow"); uint256 c = a - b; return c; } /** * @dev Returns the multiplication of two unsigned integers, reverting on * overflow. * * Counterpart to Solidity's `*` operator. * * Requirements: * - Multiplication cannot overflow. */ function mul(uint256 a, uint256 b) internal pure returns (uint256) { // Gas optimization: this is cheaper than requiring 'a' not being zero, but the // benefit is lost if 'b' is also tested. // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 if (a == 0) { return 0; } uint256 c = a * b; require(c / a == b, "SafeMath: multiplication overflow"); return c; } /** * @dev Returns the integer division of two unsigned integers. Reverts on * division by zero. The result is rounded towards zero. * * Counterpart to Solidity's `/` operator. Note: this function uses a * `revert` opcode (which leaves remaining gas untouched) while Solidity * uses an invalid opcode to revert (consuming all remaining gas). * * Requirements: * - The divisor cannot be zero. */ function div(uint256 a, uint256 b) internal pure returns (uint256) { // Solidity only automatically asserts when dividing by 0 require(b > 0, "SafeMath: division by zero"); uint256 c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } /** * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), * Reverts when dividing by zero. * * Counterpart to Solidity's `%` operator. This function uses a `revert` * opcode (which leaves remaining gas untouched) while Solidity uses an * invalid opcode to revert (consuming all remaining gas). * * Requirements: * - The divisor cannot be zero. */ function mod(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0, "SafeMath: modulo by zero"); return a % b; } }
确认后,选择上传目录,此处选择根目录 /
3.编写自己业务的合约
这里我们就把官方的demo稍微改造一下即可,代码如下: APISampleOracle.sol
pragma solidity ^0.6.0; import "./FiscoOracleClient.sol"; contract APISampleOracle is FiscoOracleClient { //指定处理的oracle address private oracleCoreAddress; // Multiply the result by 1000000000000000000 to remove decimals uint256 private timesAmount = 10**18; mapping(bytes32=>int256) private resultMap; mapping(bytes32=>bool) private validIds; int256 public result; string private url = "plain(http://blog.zhihuixiangxi.com:9999/lottery)"; constructor(address oracleAddress) public { oracleCoreAddress = oracleAddress; } function request() public returns (bytes32) { bytes32 requestId = oracleQuery(oracleCoreAddress, url, timesAmount); validIds[requestId] = true; return requestId; } /** * Receive the response in the form of int256 */ function __callback(bytes32 _requestId, int256 _result) public override onlyOracleCoreInvoke(_requestId) { require(validIds[_requestId], "id must be not used!") ; resultMap[_requestId]= _result; delete validIds[_requestId]; result = _result ; } function get() public view returns(int256){ return result; } function getById(bytes32 id) public view returns(int256){ return resultMap[id]; } function checkIdFulfilled(bytes32 id) public view returns(bool){ return validIds[id]; } function setUrl(string memory _url) public { url = _url; } function getUrl() public view returns(string memory){ return url; } }
核心就是 string private url = "plain(http://blog.zhihuixiangxi.com:9999/lottery)"; 和链下的API去交互。API的逻辑我们下一章节详聊。
4.Truora控制台刷新合约获取合约地址
我们这这里就进入ruora控制台,http://192.168.119.133:5020/#/contractSearch,获取oraclecore合约地址如下图所示:
5.部署APISampleOracle.sol
编译APISampleOracle.sol,点击部署按钮进行部署,填入上一步我们获取的合约地址即可。如下图所示:
6.测试合约
我们点击“合约调用”,选择request方法,点击确认如下图所示:
到这里,已经执行了合约并且和链下的API进行了交互。我们也可以再执行get方法进入结果的获取如下图所示: 实际我们这里可以确定我们的链下的API是不会产生小数的,可以合理的去调整合约的逻辑,但是我是选择在java项目代码里面去处理这块逻辑了,如下:
/** * 执行合约 * * @param param 合约参数 * @return */ public String handle(String param) { String url = "http://192.168.119.133:5002/WeBASE-Front/trans/handle"; String body = "{n" + " "user":"0xf0d04e0cc9b16528207027f1d5020e402096b44e",n" + " "contractName":"APISampleOracle",n" + " "contractAddress":"0x5ffbf18cfbe8c5b8fee09ccde4f5165007a6043e",n" + " "funcName":"" + param + "",n" + " "contractAbi":[{"inputs":[{"internalType":"address","name":"oracleAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"Fulfilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"Requested","type":"event"},{"inputs":[],"name":"EXPIRY_TIME","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"_requestId","type":"bytes32"},{"internalType":"int256","name":"_result","type":"int256"}],"name":"__callback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"checkIdFulfilled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"id","type":"bytes32"}],"name":"getById","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getUrl","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"request","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"result","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"_url","type":"string"}],"name":"setUrl","outputs":[],"stateMutability":"nonpayable","type":"function"}] ,n" + " "groupId" :"1",n" + " "useCns": falsen" + "}"; String res = HttpUtil.post(url, body); if (param.equals("get")) { // 处理格式 res = res.replace("[", "").replace("]", "").replace("000000000000000000", ""); } return res; }
结果也可以在truora控制台的“历史查询”里面去查看,如下图所示:
闭坑指南
问题:独立mysql整合到truora如果连接不上,页面不会有任何提示 现象:truora和链下API交互异常,返回0 解决方案:查truora后台的log,路径truora/deploy/log/server/Oracle-Service.log 会包含java.sql.SQLException: Access denied for user ‘truora’@’localhost’ (using password: YES) 的异常提示 ,在/truora/deploy/docker-compose.yml修正mysql连接参数,重启服务即可。
问题:一键部署后WeBASE-Front创建truora的合约只要重启一下服务就会消失了。
解决方案: 在webase/webase-front.yml配置文件中添加以下配置:
spring: datasource: url: jdbc:h2:file:/dist/h2/webasefront;DB_CLOSE_ON_EXIT=FALSE
然后重启一下服务即可解决。
PS:上述问题官方会在下一个版本进行修复
总结
Truora第三篇系列文章我们重点讲解《合约的开发与部署》,注意闭坑指南。其他的按照教程一步一步来成功率99%以上。