ChainLink是智能合约中重要的预言机服务项目

ChainLink可以提供大量币种之间兑换价格的实时喂价服务、随机数生成服务、API请求服务等

其中API请求服务可以调用我们自定义的预言机合约地址,并指定JobID,实现自定义的数据请求

部署ChainLink节点

本节以在Kovan测试网络中部署

准备以太坊客户端

由于ChainLink需要实时监听以太坊网络中交易状态,因此必须连接以太坊客户端,这里建议使用Infura提供的在线服务:https://infura.io/

注册账号后,在DashBoard中创建项目。即可在Keys中看到连接凭证:

image-20210412171105260

需要注意的是,后续需要使用到的是wss这个协议类型

准备PostgreSQL

ChainLink节点还依赖PostgreSQL。至少需要PostgreSQL 11以上的版本。

建议使用云数据库服务,这里以GCP的SQL服务为例:

  1. 创建实例,选择PostgreSQL

    image-20210412171849228

  2. 在实例管理界面,在”用户”选项中创建一个专用的chainlink账户,并选择密码类型,设置密码

    image-20210412172056272

    image-20210412172042102

  3. 同时添加一个对应的数据库即可

配置Docker镜像

  1. 创建文件夹

    mkdir ~/.chainlink-kovan
  2. 创建Docker环境变量文件

    echo "ROOT=/chainlink
    LOG_LEVEL=debug
    ETH_CHAIN_ID=42
    CHAINLINK_DEV=true
    MIN_OUTGOING_CONFIRMATIONS=2
    LINK_CONTRACT_ADDRESS=0xa36085F69e2889c224210F603D836748e7dC0088
    CHAINLINK_TLS_PORT=0
    SECURE_COOKIES=false
    GAS_UPDATER_ENABLED=true
    ALLOW_ORIGINS=*" > ~/.chainlink-kovan/.env
  3. 填写数据库信息

    echo "DATABASE_URL=postgresql://$USERNAME:$PASSWORD@$SERVER:$PORT/$DATABASE" >> ~/.chainlink-kovan/.env
    echo "DATABASE_TIMEOUT=0" >> ~/.chainlink-rinkeby/.env
  4. 填写以太坊客户端信息

    echo "ETH_URL=CHANGEME" >> ~/.chainlink-kovan/.env
  5. 启动节点

    cd ~/.chainlink-kovan && docker run -p 6688:6688 -v ~/.chainlink-kovan:/chainlink -it --env-file=.env smartcontract/chainlink:<version> local n

    其中Version需要到Dockerhub上查询,本文写作时最新版本为0.10.3

    第一次启动节点会生成一个节点的以太坊账户,因此需要设置账户私钥存储的KeyStore密码,同时还需要设置前端界面登录的用户名和密码

如果部署顺利的话,启动节点后其会持续见提供以太坊区块链的交易信息,日志输出如下:

image-20210412174233069

且会监听本地的6688端口,如果是搭建在远程VPS上,可以使用Nginx反向代理等方式访问,界面如下

image-20210412174544063

为节点以太坊账号申请测试币

  1. 登录到后台管理界面后,在Keys-Account addresses中可以看到当前节点的以太坊账号信息

  2. 由于我们是在Kovan测试网络中部署的节点,因此可以直接从水龙头申请测试币

    以太币:https://faucet.kovan.network/

    LINK币:https://kovan.chain.link/

  3. 领取后在节点首页可以看到更新后的代币余额:

    image-20210412174905107

一次预言机请求测试

在ChainLink中,要向预言机请求信息的用户智能合约首先需要向一个预言机节点的Oracle智能合约发起一次交易,支付本次信息查询所需的LINK代币和查询所需的必要信息。而预言机节点则会在以太坊网络中监听这个智能合约所接收到的交易,并提取对应信息,如果发现用户支付的LINK代币符合要求,则查询信息后再向Oracle智能合约发起一次交易,将数据传回区块链上。Oracle智能合约则会根据合约内保留的调用记录,调用用户智能合约的回调函数,将数据回传到用户智能合约。

其中,ChainLink节点根据一个用户可以指定的JobID进行工作。在ChainLink Market(https://market.link/)以及https://chainlinkadapters.com/中,每个公开提供服务的预言机节点都标明了自己支持的Job类型及其对应的JobID,用户可以根据自己的需求进行调用。每一个预言机的Job将用户传递的参数作为输入,并经过多个Adapter的处理后将数据回传。

因此,为了实现此次测试,我们首先需要部署我们的Oracle智能合约,其次需要部署用户合约,最后还需要配置Adapter和Job

部署Oracle智能合约

这里使用Remix进行合约部署。

直接部署下面的代码即可:

pragma solidity 0.4.24;

import "https://github.com/smartcontractkit/chainlink/evm-contracts/src/v0.4/Oracle.sol";

需要注意的是,Oracle并不会接受任何节点的数据回传请求。因此我们需要先把我们预言机节点的地址加入白名单。可以通过调用合约中的setFulfillmentPermisson函数实现,需要注意的是,必须要合约部署者才有权利和其交互。在node中填入节点账户地址,allowed中填写true即可

image-20210412180431886

除此之外,由于用户支付的LINK代币会锁在该合约中,只有合约创建者才能调用withdraw函数取出其中的LINK代币

部署用户合约

这里部署一个模拟使用预言机的用户合约,合约代码如下(由ChainLink官方提供):

// This example code is designed to quickly deploy an example contract using Remix.

pragma solidity ^0.6.0;

//import "https://raw.githubusercontent.com/smartcontractkit/chainlink/master/evm-contracts/src/v0.6/ChainlinkClient.sol";
import "https://raw.githubusercontent.com/smartcontractkit/chainlink/develop/evm-contracts/src/v0.6/ChainlinkClient.sol";


contract APIConsumer is ChainlinkClient {

uint256 public volume;

address private oracle;
bytes32 private jobId;
uint256 private fee;

/**
* Network: Kovan
* Chainlink - 0x2f90A6D021db21e1B2A077c5a37B3C7E75D15b7e
* Chainlink - 29fa9aa13bf1468788b7cc4a500a45b8
* Fee: 0.1 LINK
*/
constructor() public {
setPublicChainlinkToken();
oracle = 0x;
jobId = "4d9786181e964409ab8d4c336e22a147";
fee = 1 * 10 ** 18; // 1 LINK
}

/**
* Create a Chainlink request to retrieve API response, find the target
* data, then multiply by 1000000000000000000 (to remove decimal places from data).
************************************************************************************
* STOP! *
* THIS FUNCTION WILL FAIL IF THIS CONTRACT DOES NOT OWN LINK *
* ---------------------------------------------------------- *
* Learn how to obtain testnet LINK and fund this contract: *
* ------- https://docs.chain.link/docs/acquire-link -------- *
* ---- https://docs.chain.link/docs/fund-your-contract ----- *
* *
************************************************************************************/
function requestVolumeData() public returns (bytes32 requestId)
{
Chainlink.Request memory request = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);

// Set the URL to perform the GET request on
request.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD");

// Set the path to find the desired data in the API response, where the response format is:
// {"RAW":
// {"ETH":
// {"USD":
// {
// ...,
// "VOLUME24HOUR": xxx.xxx,
// ...
// }
// }
// }
// }
request.add("path", "RAW.ETH.USD.VOLUME24HOUR");

// Multiply the result by 1000000000000000000 to remove decimals
int timesAmount = 10**18;
request.addInt("times", timesAmount);

// Sends the request
return sendChainlinkRequestTo(oracle, request, fee);
}

/**
* Receive the response in the form of uint256
*/
function fulfill(bytes32 _requestId, uint256 _volume) public recordChainlinkFulfillment(_requestId)
{
volume = _volume;
}

/**
* Withdraw LINK from this contract
*
* NOTE: DO NOT USE THIS IN PRODUCTION AS IT CAN BE CALLED BY ANY ADDRESS.
* THIS IS PURELY FOR EXAMPLE PURPOSES ONLY.
*/
function withdrawLink() external {
LinkTokenInterface linkToken = LinkTokenInterface(chainlinkTokenAddress());
require(linkToken.transfer(msg.sender, linkToken.balanceOf(address(this))), "Unable to transfer");
}
}

需要注意的是,其中的oracle地址要修改为你部署的oracle合约地址。除此之外,JobID也要更改为后面部署的Job的对应ID

部署Job

接下来要在我们的ChainLink节点中部署对应的Job,从上面的合约代码可以发现,用户想要完成的是请求一个HTTP API,并解析其中的JSON字符串,再转换为Solidity的uint256数据结构返回。

这些使用ChainLink自带的Adapter都可以有效实现。

  1. 在ChainLink节点后台页面的Jobs中添加一个Job,Job代码如下:

    {
    "name": "Get > Uint256",
    "initiators": [
    {
    "type": "runlog",
    "params": {
    "address": "0x"
    }
    }
    ],
    "tasks": [
    {
    "type": "httpget"
    },
    {
    "type": "jsonparse"
    },
    {
    "type": "multiply"
    },
    {
    "type": "ethuint256"
    },
    {
    "type": "ethtx"
    }
    ]
    }

    其中,httpget模块实现的是从用户提供的URL请求内容的功能,jsonparse则是对其进行JSON解析,并按照用户合约提供的path抽取数据,再将抽取出的数据通过multiply模块乘一定倍数,避免存在小数的情况。最后两个模块将数据转换为Solidity的uint256数据结构并发起交易。

  2. 创建Job后会在后台获取到一个JobID,应该将这个ID填入到用户合约的对应部分:

    image-20210412181650485

开始请求

接下来调用用户合约,就可以等待预言机返回结果了。

在预言机后台的Runs中,可以看到本次调用的结果和各个阶段Adapter的处理结果:

image-20210412181841331

第三方Adapter的部署

ChainLink节点自带的Adapter能实现的功能有限。但其提供的Bridges功能允许我们链接其他的外部Adapter。ChainLink节点会按照一定规范把用户传入的参数以JSON形式发送给外部节点,外部节点处理后返回结果即可。

以CL-Adapters中的Asset Price External Adaptor为例:https://chainlinkadapters.com/details/linkpoolio/asset-price-cl-ea

按照官方文档启动起来外部Adapter后其会在服务器上监听一个端口,需要注意的是,使用上面方法启动的ChainLink节点运行在Docker容器内,访问主机上运行的服务不能使用127.0.0.1,而应该使用对应的内网IP或者公网IP

  1. 我们在Bridegs中添加上述信息,并给Adapter命名:

    image-20210412182324434

  2. 接下来编写Job如下:

    {
    "name": "Get > Uint256",
    "initiators": [
    {
    "type": "runlog",
    "params": {
    "address": "0x47a9ee90b4eea3f976d5c2b40df0979c7c40f05e"
    }
    }
    ],
    "tasks": [
    {
    "type": "pricegetter",
    "confirmations": 0
    },
    {
    "type": "copy"
    },
    {
    "type": "multiply"
    },
    {
    "type": "ethuint256"
    },
    {
    "type": "ethtx"
    }
    ]
    }

    其中,copy模块是根据用户传入的copyPath参数,解析外部Adapter返回的结果。

  3. 修改用户合约的传参部分如下:

    Chainlink.Request memory req = buildChainlinkRequest(jobId, this, this.fulfill.selector);
    req.add("base", "LINK");
    req.add("quote", "BTC");
    req.add("copyPath", "price");
    req.addInt("times", 100000000);
  4. 最后发起调用,在预言机节点中看到成功日志

    image-20210412182536555