通过预言机获取任意链下数据 - Chainlink Any API 代码解析

除了获取通证价格以外,开发者还可以通过 Chainlink Any API 获得任何个性化数据。文章讲解了如何新建自己的 Chainlink 节点,并且在合约中使用 Chainlink 节点 AnyAPI 服务获得个性化数据。

智能合约对链下数据的兼容会大大增加开发复杂度,Chainlink 通过 AnyAPI 使开发者的智能合约可以通过去中心化预言机网络(Decentralized Oracle Network:DON)获取外部数据。这样在使用 Chainlink AnyAPI 的时候,开发人员可以投入最少的开发资源,获得最大的自由度,因此可以更加专注在智能合约的功能性上,而非怎么样去获取数据上。

虽然 Chainlink Data Feed 可以给链上智能合约提供由 DON 聚合以后的通证价格,但是在很多场景下,尤其是非 DeFi 应用中,dApp 除了价格以外还需要多种多样的数据来实现自己的业务逻辑。比如在保险领域,智能合约需要天气数据来计算参保方的赔付金额,在合成资产协议中,外部股票市场的数据是必不可少的,除此以外,随着 web3 的场景越来越丰富,会越来越多地依赖于链下数据,比如说链下的交通运输,房地产,身份信息等等多种多样的数据。

如果你的智能合约需要依赖于这些数据,Chainlink AnyAPI 都可以作为一个工具让你从指定的外部数据源获取到特定数据。接下来,就让我们看看 Chainlink AnyAPI 的工作原理是什么。

使用 Chainlink AnyAPI 服务

发送 Chainlink 请求

在从 Chainlink 预言机节点获得数据之前,我们首先需要创建一个用户合约,然后在用户合约中给 Chainlink 预言机节点发送一个请求。下面的代码将展示如何通过用户合约给预言机节点发送请求:

function requestVolumeData() public returns (bytes32 requestId) {
 Chainlink.Request memory req = buildChainlinkRequest(jobId, address(this), this.fulfill.selector);
 req.add('get', 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD');
 req.add('path', 'RAW,ETH,USD,VOLUME24HOUR');
 int256 timesAmount = 10**18;
 req.addInt('times', timesAmount);
 return sendChainlinkRequest(req, fee);
}

Chainlink AnyAPI 获取的数据的方式一般是通过预言机节点给外部数据源发送 RESTful 请求,所以节点在发送请求之前需要知道要请求的数据的 API 和参数。为了给 Any API 提供必要的数据,在智能合约的 ChainlinkRequest 中,我们需要通过函数 buildChainlinkRequest 加入这些相关信息。buildChainlinkRequest 这个函数定义在 ChainlinkClient.sol,代码展示如下:

function buildChainlinkRequest(
 bytes32 specId,
 address callbackAddr,
 bytes4 callbackFunctionSignature)
internal
pure
returns (Chainlink.Request memory)
{
    Chainlink.Request memory req;
    return req.initialize(specId, callbackAddr, callbackFunctionSignature);
}

buildChainlinkRequest 函数中,所有与请求相关的信息都会加入到 Request 这个结构体中,并且调用函数 initialize 来完成初始化。

Struct Request is defined in Chainlink.sol as below:
struct Request {
 bytes32 id;
 address callbackAddress;
 bytes4 callbackFunctionId;
 uint256 nonce;
 BufferChainlink.buffer buf;
}

函数 initialize 也定义在 Chainlink.sol 文件中,代码如下:

function initialize(
 Request memory self,
 bytes32 jobId,
 address callbackAddr,
 bytes4 callbackFunc
)
internal
pure
returns (Chainlink.Request memory)
{
  BufferChainlink.init(self.buf, defaultBufferSize);
  self.id = jobId;
  self.callbackAddress = callbackAddr;
  self.callbackFunctionId = callbackFunc;
  return self;
}

在函数 buildChainlinkRequest 中,会接受 3 个参数:

  • jobId: 预言机节点需要执行的 job 的 ID。
  • callbackAddr:用户合约地址,这个参数是预言机节点将会将返回数据的合约地址。
  • callbackFunc:这个是预言机节点需要回调的函数签名。

在这三个参数设置好以后,还需要加入 URL 和数据路径,因为我们需要告诉预言机节点通过哪个 API 获取数据,并且在获取数据以后,如何在返回的数据中找到我们所需要的有效数据。

req.add('get', 'https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD');
req.add('path', 'RAW,ETH,USD,VOLUME24HOUR');

这里的 path 参数很重要,因为预言机通常会通过用户提供的 URL 获得庞大而杂乱的数据,下面的 JSON 数据就是一个例子。

{"RAW":
  {"ETH":
    {"USD":
     {"TYPE":"5",
      "MARKET":"CCCAGG",
      "FROMSYMBOL":"ETH",
      "TOSYMBOL":"USD",
      "FLAGS":"2049",
      "PRICE":1083.43,
      "LASTUPDATE":1655472805,
      "MEDIAN":1083.49,
      "LASTVOLUME":0.01152796,
      "LASTVOLUMETO":12.488815466,
      "LASTTRADEID":"298687546",
      "VOLUMEDAY":279647.314121829,
      "VOLUMEDAYTO":304661021.9293905,
      "VOLUME24HOUR":617393.32461219,
      .......
      .......
      "TOTALTOPTIERVOLUME24HTO":"$ 5.47B",
      "IMAGEURL":"/media/37746238/eth.png"}
     }
   }
}

通过在 request 中的 path 数据,预言机节点才可以获取到我们所想要的数据。所以在 URL 和路径被设置好以后,这个 chainlinkRequest 就完成并且可以被发送了。 return sendChainlinkRequest(req, fee); 这里的调用顺序是: 终端用户会在用户合约中调用函数 requestVolumeData,然后用户合约会调用 ChainlinkClient.sol 中的函数 sendChainlinkRequest。 然后 sendChainlinkRequest 会调用函数 sendChainlinkRequestTo,这个函数会接受的参数是预言机的地址,函数签名和其他相关信息,然后 encode 所有的信息,转化为 bytes 数据。 接下来,_rawRequest 会调用 Link 通证合约的中的 transferAndCall 函数,transferAndCall 是 ERC-677 标准中所定义的函数。transferAndCall会把要执行的代码(上一步中的 encode 数据)发送给预言机合约,然后要求该合约执行 代码逻辑。 最后,预言机合约 OperatorInterface.sol中的函数 operatorRequest 会被上一步中的 transferAndCall调用,然后该函数会将函数签名,requestId 等信息写到 event 中,以便链下预言机发现。 让我们看一个具体的例子,Chainlink 官方在测试网 Kovan 中所部署了一个预言机合约。这个合约的代码可以在这里看到。

 function operatorRequest(
    address sender,
    uint256 payment,
    bytes32 specId,
    bytes4 callbackFunctionId,
    uint256 nonce,
    uint256 dataVersion,
    bytes calldata data
  ) external override validateFromLINK {
    (bytes32 requestId, uint256 expiration) = _verifyAndProcessOracleRequest(
      sender,
      payment,
      sender,
      callbackFunctionId,
      nonce,
      dataVersion
    );
    emit OracleRequest(specId, sender, requestId, payment, sender, callbackFunctionId, expiration, dataVersion, data);
  }

在代码中,可以很容易看到,这个函数就是把所有的信息写到了 event log 中,然后等待链下的预言机节点检测。

返回请求数据

返回数据的函数 fulfillOracleRequest2 也被定义在 OperatorInterface.sol 文件中。

function fulfillOracleRequest2(
bytes32 requestId,
uint256 payment,
address callbackAddress,
bytes4 callbackFunctionId,
uint256 expiration,
bytes calldata data
) external returns (bool);

让我们看看刚才的合约中,这个函数中的逻辑是什么样的:

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");

   (bool success, ) = callbackAddress.call(abi.encodePacked(callbackFunctionId, data));
   return success;
 }

通过上面的代码,我们看到这个函数调用了 callbackAddress 地址合约中的一个函数,这个地址就是之前传给预言机合约的用户合约地址。用户合约中 fullfill 函数被调用了:

function fulfill(bytes32 _requestId, uint256 _volume) public recordChainlinkFulfillment(_requestId) {
       emit RequestVolume(_requestId, _volume);
       volume = _volume;
   }

这个展示合约中的 fulfill 函数非常简单,就是将 requestId 和 volume 的数据写入 event log,然后将 volume 写入到本地变量 _volume 中。

运行 Chainlink 节点提供 Any API 服务

在上一章节,我们了解了如何在用户合约中使用 Chainlink AnyAPI服务,以获取到多种多样的数据。接下来,我们来看看如何去运行 Chainlink AnyAPI 的“后端”,来看看如何运行一个预言机节点来帮助链上合约获取它们所需要的各种数据。

运行自己的 Chainlink 节点

新建节点有很多种方式,如果你是一个运维人员并且自身有足够的硬件资源,可以根据官方文档中的教程新建一个节点。如果你不想要自己运维节点,那么可以选择使用 naas.link(node as a service),只需要点几个按钮就可以新建一个节点,并且是免费的。另外,Chainlink 官方开发者关系团队也在不同的链上维护了一些 Chainlink 节点,可以在这里查看关于这些节点的 JobId,合约地址和其他相关信息。 如果你需要一些特殊的数据,比如说天气数据,股票数据,体育比赛数据等等,可以在 Chainlink 提供的数据市场 market.link 中搜索。

如果你对于Chainlink 节点所提供的数据有更个性化的要求,可以登陆 Chainlink 的 Discord,Chainlink 团队会帮你在社区中联系节点运营商,以满足你的需求。华语开发者也可以直接联系 Chainlink 中国团队,获得更快的响应。

在上一章节,我们了解了如何去写一个用户合约来使用 Chainlink AnyAPI ,从而获取到多种多样的数据。接下来,我们可以学习一下如何去运行 AnyAPI 的“后端”,来看看如何运行一个预言机节点来帮助链上合约获取它们所需要的各种数据。

预言机节点的 job 和 task pipeline (TOML)

TOML(Tom's Obviously Minimal Language)是一种配置文件的格式,因为 sematics 比较清晰,所以更容易阅读,被很多项目所使用。Chainlink 节点就是使用 TOML 来定义节点所提供的 API 服务所对应的 job 的详细信息。

在 Chainlink 节点中,每一个 jobId 都会代表一个在节点中运行的 job。比如说在 API 样例代码中,jobId 代表的 job 是获取 BTC 昨天的市场数据的。Chainlink 节点使用 TOML 来定义怎么样从 API 中获取数据,并且将这个数据进行标准化,使其可以被用户合约使用。

Chainlink 节点做的任何操作都会依赖于 job,现在支持以下 6 种 job:

  1. Cron:根据一个时间表而非外部触发来执行一个 job。
  2. Direct request job:根据一个用户所发出的请求的 receipt 来执行一个 job。预言机合约会在被 emit 的 log 中发现用户的请求。这个方式和以前 ethlog/runlog 执行 job 的方式类似。
  3. Flux monitor job:根据不同的预言机节点所返回的数据来更新 data feed 中的数据。更新会在波动率足够大,或者是 heartbeat 超出时间限制的时候触发。
  4. Keeper job:根据链上合约中的状态进行判断,判断成功以后会执行智能合约中的函数,可以非定期地调用合约中的函数。
  5. Off-chain reporting job:Off-chain reporting(OCR)和 Flux monitor job 非常类似,OCR job 会根据多个 Chainlink 预言机节点聚合以后的数据更新 data feed。OCR 和 Flux monitor job 的区别是 OCR 使用了由密码学保证的链下协议,可以让单个节点在一个 round 中将所有其他节点中的数据提交上来。通过这个方式,可以节省大量的gas。
  6. Webhook job: Webhook 可以由 HTTP 请求所触发,HTTP 请求可以由用户或者其他外部触发器所触发。

在 job 中,需要定义以下变量:

  1. name: 在 Chainlink 节点 UI 中所现实的 job 的名字。
  2. type: job 的类型,可以是上述 job 类型中的任何一个。
  3. schemaVersion: 现在都需要设置为 1,这个设置是为了让 job 的格式向前兼容。
  4. observationSource: 这个参数定义 job 具体要做的操作。
  5. maxTaskDuration: 任何一个任务能够运行的最长事件默认值。如果一个任务达到了最长时间,这个任务就会报错。
  6. externalJobID: 提供了一种可选方法,用户可以通过这个参数直接定义 job 的。

除了上述参数以外,你还需要定义 job 中的任务(task),让我们看看一个 job 的 TOML 文件的例子:

type                = "directrequest"
schemaVersion       = 1
evmChainID          = 1
name                = "example eth request event spec"
contractAddress     = "0x613a38AC1659769640aaE063C651F48E0250454C"

observationSource   = """
   ds          [type="http" method=GET url="http://example.com"]
   ds_parse    [type="jsonparse" path="USD"]
   ds_multiply [type="multiply" times=100]

   ds -> ds_parse -> ds_multiply

ds, ds_parse, ds_multiply 是 job 要执行的 3 个任务,执行顺序通过 ds -> ds_parse -> ds_multiply 这一行定义,语法非常简单,即先给 “http://exmpale.com” 发送 GET 请求,然后使用路径 “USD” 来找到用户在这个JSON 文件中需要的值。这个 JSON 文件如下:

{
  usd: number
}

最后,这个 job 会根据 ds_multiply 这个任务将结果乘以 100。

总结

除了可以通过 Chainlink data feed 获取通证价格以外,开发者还可以通过 Chainlink Any API 获得任何个性化数据。文章讲解了如何新建自己的 Chainlink 节点,并且在合约中使用 Chainlink 节点 AnyAPI 服务获得个性化数据。 您可以关注 Chainlink 预言机并且私信加入开发者社区,有大量关于智能合约的学习资料以及关于区块链的话题!

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-09-20 20:54
  • 阅读 ( 416 )
  • 学分 ( 4 )
  • 分类:预言机

0 条评论

请先 登录 后评论
Chainlink
Chainlink

116 篇文章, 2071 学分