HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • Web3 完整技术体系

    • Web3 完整技术体系
    • 区块链基础与密码学
    • 第一章:比特币原理与实现
    • 第二章:以太坊架构与核心概念
    • Solidity智能合约开发基础
    • 04-Solidity进阶与安全
    • 05-ERC标准详解
    • 06-DeFi核心协议-去中心化交易所
    • 07-DeFi核心协议-借贷协议
    • 08-DeFi核心协议-稳定币
    • NFT与元宇宙
    • Layer2扩容方案
    • 跨链技术
    • Web3前端开发
    • 链下数据与预言机
    • 智能合约测试与部署
    • MEV与交易优化
    • DAO治理
    • 项目实战:完整DeFi借贷协议

链下数据与预言机

章节导读

智能合约运行在封闭的区块链环境中,无法主动访问外部世界的数据。这就是所谓的"预言机问题"(Oracle Problem)。预言机作为区块链与现实世界的桥梁,为智能合约提供可信的链下数据,使得DeFi、保险、游戏等应用成为可能。

本章将深入讲解预言机的原理、Chainlink的架构和使用、The Graph的索引查询,以及如何在DApp中集成这些服务。我们还会学习价格预言机、随机数生成、链下计算等实际应用场景。

学习路线

预言机基础
├── 预言机问题
├── 信任模型
├── 去中心化预言机
└── 预言机类型

Chainlink
├── Data Feeds(价格)
├── VRF(随机数)
├── Automation(自动化)
└── Functions(链下计算)

The Graph
├── Subgraph
├── GraphQL查询
├── 索引与订阅
└── 实战应用

实战项目
├── 价格预言机DApp
├── NFT盲盒(VRF)
└── 链下数据查询

一、预言机问题

1.1 什么是预言机问题

区块链的局限性

智能合约无法:
 访问HTTP API
 读取传统数据库
 获取链下事件
 生成真随机数
 执行复杂计算

为什么需要预言机

DeFi应用需要:
 资产价格(ETH/USD, BTC/USD)
 利率数据
 汇率数据

保险应用需要:
 天气数据
 航班信息
 灾害事件

游戏应用需要:
 真随机数
 链下计算结果

其他应用需要:
 体育赛事结果
 选举结果
 供应链数据

1.2 中心化预言机的风险

单点故障

// 不安全: 中心化预言机
contract CentralizedOracle {
    address public oracle;
    mapping(string => uint256) public prices;

    // 只有预言机可以更新价格
    function updatePrice(string memory symbol, uint256 price) external {
        require(msg.sender == oracle, "Not oracle");
        prices[symbol] = price;
    }

    // 问题:
    // 1. 如果oracle账户被盗,攻击者可以操纵价格
    // 2. oracle下线,价格无法更新
    // 3. oracle作恶,提供虚假数据
}

1.3 去中心化预言机解决方案

Chainlink的方法

去中心化预言机网络(DON)
├── 多个独立的节点
├── 数据聚合
├── 信誉系统
└── 惩罚机制

工作流程:
1. 合约请求数据
2. 多个节点独立获取数据
3. 节点提交数据到聚合合约
4. 聚合合约计算中位数/平均值
5. 结果返回给请求合约

二、Chainlink Data Feeds

2.1 价格预言机

Chainlink提供了数百个价格对的实时数据,如ETH/USD、BTC/USD等。

使用Price Feed

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceConsumer {
    AggregatorV3Interface internal priceFeed;

    /**
     * Network: Ethereum Mainnet
     * Aggregator: ETH/USD
     * Address: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
     */
    constructor() {
        priceFeed = AggregatorV3Interface(
            0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
        );
    }

    /**
     * 获取最新价格
     */
    function getLatestPrice() public view returns (int) {
        (
            uint80 roundId,
            int price,
            uint startedAt,
            uint updatedAt,
            uint80 answeredInRound
        ) = priceFeed.latestRoundData();

        // price有8位小数
        // 例如: 2000.12345678 USD = 200012345678
        return price;
    }

    /**
     * 获取价格(带小数点)
     */
    function getLatestPriceWithDecimals() public view returns (uint256) {
        (, int price, , ,) = priceFeed.latestRoundData();
        uint8 decimals = priceFeed.decimals();

        // 转换为18位小数
        return uint256(price) * 10 ** (18 - decimals);
    }

    /**
     * 获取历史价格
     */
    function getHistoricalPrice(uint80 roundId) public view returns (int) {
        (
            uint80 id,
            int price,
            uint startedAt,
            uint updatedAt,
            uint80 answeredInRound
        ) = priceFeed.getRoundData(roundId);

        return price;
    }
}

2.2 实战:清算机器人

使用Chainlink Price Feed实现一个简单的借贷协议清算功能。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract SimpleLending {
    AggregatorV3Interface public priceFeed;

    // 抵押品(ETH) -> 借款(USDC)
    struct Position {
        uint256 collateral; // ETH数量
        uint256 debt;       // USDC数量
    }

    mapping(address => Position) public positions;

    // 抵押率要求(150%)
    uint256 public constant COLLATERAL_RATIO = 150;

    // 清算奖励(10%)
    uint256 public constant LIQUIDATION_BONUS = 10;

    event Deposited(address indexed user, uint256 amount);
    event Borrowed(address indexed user, uint256 amount);
    event Liquidated(address indexed user, address indexed liquidator, uint256 amount);

    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    // 存入ETH作为抵押品
    function deposit() external payable {
        require(msg.value > 0, "Cannot deposit 0");
        positions[msg.sender].collateral += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    // 借出USDC
    function borrow(uint256 usdcAmount) external {
        Position storage position = positions[msg.sender];
        require(position.collateral > 0, "No collateral");

        // 检查抵押率
        uint256 maxBorrow = getMaxBorrow(msg.sender);
        require(position.debt + usdcAmount <= maxBorrow, "Insufficient collateral");

        position.debt += usdcAmount;

        // 实际应该转移USDC给用户
        // IERC20(usdc).transfer(msg.sender, usdcAmount);

        emit Borrowed(msg.sender, usdcAmount);
    }

    // 计算最大可借金额
    function getMaxBorrow(address user) public view returns (uint256) {
        Position memory position = positions[user];

        // 获取ETH价格
        (, int price, , ,) = priceFeed.latestRoundData();
        require(price > 0, "Invalid price");

        // ETH价格(8位小数) * ETH数量(18位小数) / 抵押率
        // 结果单位: USDC(6位小数)
        uint256 collateralValue = uint256(price) * position.collateral / 1e8; // USD value
        uint256 maxBorrow = collateralValue * 100 / COLLATERAL_RATIO / 1e12; // Convert to USDC decimals

        if (maxBorrow > position.debt) {
            return maxBorrow - position.debt;
        }
        return 0;
    }

    // 检查是否可被清算
    function isLiquidatable(address user) public view returns (bool) {
        Position memory position = positions[user];
        if (position.debt == 0) return false;

        // 当前抵押率
        (, int price, , ,) = priceFeed.latestRoundData();
        uint256 collateralValue = uint256(price) * position.collateral / 1e8;
        uint256 currentRatio = collateralValue * 100 / (position.debt * 1e12);

        // 抵押率低于150%可被清算
        return currentRatio < COLLATERAL_RATIO;
    }

    // 清算
    function liquidate(address user) external {
        require(isLiquidatable(user), "Cannot liquidate");

        Position storage position = positions[user];
        uint256 debt = position.debt;
        uint256 collateral = position.collateral;

        // 清空仓位
        position.debt = 0;
        position.collateral = 0;

        // 计算清算奖励
        (, int price, , ,) = priceFeed.latestRoundData();
        uint256 debtInEth = debt * 1e12 * 1e8 / uint256(price);
        uint256 bonus = debtInEth * LIQUIDATION_BONUS / 100;
        uint256 totalPayment = debtInEth + bonus;

        // 转移抵押品给清算者
        payable(msg.sender).transfer(totalPayment);

        // 剩余抵押品返还给用户
        if (collateral > totalPayment) {
            payable(user).transfer(collateral - totalPayment);
        }

        emit Liquidated(user, msg.sender, debt);
    }
}

2.3 多个价格源

使用多个价格源提高安全性

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract MultiSourcePriceOracle {
    AggregatorV3Interface[] public priceFeeds;

    // 价格偏差阈值(5%)
    uint256 public constant MAX_DEVIATION = 5;

    constructor(address[] memory _priceFeeds) {
        for (uint i = 0; i < _priceFeeds.length; i++) {
            priceFeeds.push(AggregatorV3Interface(_priceFeeds[i]));
        }
    }

    // 获取聚合价格(中位数)
    function getAggregatedPrice() public view returns (uint256) {
        require(priceFeeds.length > 0, "No price feeds");

        uint256[] memory prices = new uint256[](priceFeeds.length);

        // 获取所有价格
        for (uint i = 0; i < priceFeeds.length; i++) {
            (, int price, , uint updatedAt,) = priceFeeds[i].latestRoundData();

            // 检查价格更新时间(不超过1小时)
            require(block.timestamp - updatedAt < 3600, "Stale price");
            require(price > 0, "Invalid price");

            prices[i] = uint256(price);
        }

        // 计算中位数
        uint256 median = getMedian(prices);

        // 检查所有价格与中位数的偏差
        for (uint i = 0; i < prices.length; i++) {
            uint256 deviation = prices[i] > median
                ? (prices[i] - median) * 100 / median
                : (median - prices[i]) * 100 / median;

            require(deviation <= MAX_DEVIATION, "Price deviation too large");
        }

        return median;
    }

    // 计算中位数
    function getMedian(uint256[] memory array) internal pure returns (uint256) {
        // 排序
        for (uint i = 0; i < array.length; i++) {
            for (uint j = i + 1; j < array.length; j++) {
                if (array[i] > array[j]) {
                    uint256 temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            }
        }

        // 返回中位数
        if (array.length % 2 == 0) {
            return (array[array.length / 2 - 1] + array[array.length / 2]) / 2;
        } else {
            return array[array.length / 2];
        }
    }
}

三、Chainlink VRF(可验证随机数)

3.1 为什么需要链上随机数

伪随机数的问题

// 不安全: 矿工可以操纵
function unsafeRandom() public view returns (uint256) {
    return uint256(keccak256(abi.encodePacked(
        block.timestamp,
        block.difficulty,
        msg.sender
    )));
}

// 问题:
// 1. 矿工可以选择性地包含/排除交易
// 2. 矿工可以操纵block.timestamp(±15秒)
// 3. 可预测(攻击者可以在同一个区块内模拟)

Chainlink VRF的优势

 真随机数(基于密码学)
 可验证(任何人都能验证)
 无法操纵(即使是节点运营商)
 链上验证

3.2 使用Chainlink VRF

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";

contract RandomNumberConsumer is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;

    // VRF配置
    uint64 subscriptionId;
    bytes32 keyHash;
    uint32 callbackGasLimit = 200000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 1; // 请求的随机数数量

    // 请求ID -> 用户地址
    mapping(uint256 => address) public requestIdToSender;

    // 用户地址 -> 随机数
    mapping(address => uint256) public randomResults;

    event RandomRequested(uint256 requestId, address requester);
    event RandomFulfilled(uint256 requestId, uint256 randomNumber);

    /**
     * Network: Ethereum Sepolia Testnet
     * VRF Coordinator: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
     * Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     */
    constructor(uint64 _subscriptionId)
        VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625)
    {
        COORDINATOR = VRFCoordinatorV2Interface(
            0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
        );
        subscriptionId = _subscriptionId;
        keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
    }

    // 请求随机数
    function requestRandomWords() external returns (uint256 requestId) {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );

        requestIdToSender[requestId] = msg.sender;

        emit RandomRequested(requestId, msg.sender);
    }

    // VRF回调函数
    function fulfillRandomWords(
        uint256 requestId,
        uint256[] memory randomWords
    ) internal override {
        address sender = requestIdToSender[requestId];
        randomResults[sender] = randomWords[0];

        emit RandomFulfilled(requestId, randomWords[0]);
    }

    // 获取随机数
    function getRandomResult(address user) external view returns (uint256) {
        return randomResults[user];
    }
}

3.3 实战:抽奖合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";

contract Lottery is VRFConsumerBaseV2 {
    VRFCoordinatorV2Interface COORDINATOR;

    uint64 subscriptionId;
    bytes32 keyHash;
    uint32 callbackGasLimit = 300000;
    uint16 requestConfirmations = 3;
    uint32 numWords = 1;

    address public owner;
    address[] public players;
    address public recentWinner;

    enum LotteryState { OPEN, CALCULATING }
    LotteryState public lotteryState;

    uint256 public entranceFee = 0.01 ether;

    event LotteryEntered(address indexed player);
    event WinnerPicked(address indexed winner, uint256 amount);

    constructor(uint64 _subscriptionId, address _vrfCoordinator, bytes32 _keyHash)
        VRFConsumerBaseV2(_vrfCoordinator)
    {
        owner = msg.sender;
        COORDINATOR = VRFCoordinatorV2Interface(_vrfCoordinator);
        subscriptionId = _subscriptionId;
        keyHash = _keyHash;
        lotteryState = LotteryState.OPEN;
    }

    // 参与抽奖
    function enterLottery() external payable {
        require(lotteryState == LotteryState.OPEN, "Lottery not open");
        require(msg.value >= entranceFee, "Not enough ETH");

        players.push(msg.sender);

        emit LotteryEntered(msg.sender);
    }

    // 开始抽奖(只有owner可以调用)
    function pickWinner() external {
        require(msg.sender == owner, "Not owner");
        require(lotteryState == LotteryState.OPEN, "Already calculating");
        require(players.length > 0, "No players");

        lotteryState = LotteryState.CALCULATING;

        // 请求随机数
        COORDINATOR.requestRandomWords(
            keyHash,
            subscriptionId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
    }

    // VRF回调
    function fulfillRandomWords(
        uint256 /* requestId */,
        uint256[] memory randomWords
    ) internal override {
        // 选择获奖者
        uint256 indexOfWinner = randomWords[0] % players.length;
        address winner = players[indexOfWinner];
        recentWinner = winner;

        // 转账奖金
        uint256 prize = address(this).balance;
        (bool success, ) = winner.call{value: prize}("");
        require(success, "Transfer failed");

        // 重置
        players = new address[](0);
        lotteryState = LotteryState.OPEN;

        emit WinnerPicked(winner, prize);
    }

    // 查询参与者数量
    function getNumberOfPlayers() external view returns (uint256) {
        return players.length;
    }

    // 查询奖池金额
    function getPrizePool() external view returns (uint256) {
        return address(this).balance;
    }
}

前端集成

import { ethers } from 'ethers';

async function enterLottery() {
    const lottery = new ethers.Contract(LOTTERY_ADDRESS, LOTTERY_ABI, signer);

    const entranceFee = await lottery.entranceFee();

    const tx = await lottery.enterLottery({ value: entranceFee });
    await tx.wait();

    console.log('Entered lottery!');
}

async function pickWinner() {
    const lottery = new ethers.Contract(LOTTERY_ADDRESS, LOTTERY_ABI, signer);

    const tx = await lottery.pickWinner();
    await tx.wait();

    console.log('Picking winner...');

    // 监听WinnerPicked事件
    lottery.on('WinnerPicked', (winner, amount) => {
        console.log(`Winner: ${winner}, Prize: ${ethers.formatEther(amount)} ETH`);
    });
}

四、Chainlink Automation

4.1 自动化任务

Chainlink Automation(原Chainlink Keepers)允许智能合约自动执行维护任务。

应用场景

 定期清算(DeFi借贷)
 定期收益分配
 自动复利
 拍卖结束
 游戏回合结束

4.2 实现自动化合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";

contract AutoLiquidation is AutomationCompatibleInterface {
    address[] public positions;
    mapping(address => Position) public userPositions;

    struct Position {
        uint256 collateral;
        uint256 debt;
        bool active;
    }

    // Chainlink Automation调用此函数检查是否需要执行
    function checkUpkeep(bytes calldata /* checkData */)
        external
        view
        override
        returns (bool upkeepNeeded, bytes memory performData)
    {
        // 查找需要清算的仓位
        address[] memory toLiquidate = new address[](positions.length);
        uint256 count = 0;

        for (uint i = 0; i < positions.length; i++) {
            address user = positions[i];
            if (isLiquidatable(user)) {
                toLiquidate[count] = user;
                count++;
            }
        }

        if (count > 0) {
            // 返回需要清算的地址列表
            address[] memory result = new address[](count);
            for (uint i = 0; i < count; i++) {
                result[i] = toLiquidate[i];
            }

            upkeepNeeded = true;
            performData = abi.encode(result);
        } else {
            upkeepNeeded = false;
        }
    }

    // Chainlink Automation调用此函数执行任务
    function performUpkeep(bytes calldata performData) external override {
        address[] memory toLiquidate = abi.decode(performData, (address[]));

        for (uint i = 0; i < toLiquidate.length; i++) {
            if (isLiquidatable(toLiquidate[i])) {
                liquidate(toLiquidate[i]);
            }
        }
    }

    // 检查是否可清算
    function isLiquidatable(address user) public view returns (bool) {
        Position memory pos = userPositions[user];
        if (!pos.active || pos.debt == 0) return false;

        // 简化: 假设抵押率低于150%可清算
        return pos.collateral * 100 / pos.debt < 150;
    }

    // 清算
    function liquidate(address user) internal {
        Position storage pos = userPositions[user];
        pos.active = false;
        // 清算逻辑...
    }

    // 其他函数...
}

五、The Graph

5.1 什么是The Graph

The Graph是一个去中心化的索引和查询协议,用于区块链数据。

为什么需要The Graph

问题:
 直接查询区块链效率低
 无法进行复杂查询
 历史数据难以获取

解决:
 索引区块链事件
 提供GraphQL查询接口
 支持复杂查询和聚合
 实时订阅更新

5.2 创建Subgraph

定义Schema(schema.graphql)

type Token @entity {
  id: ID!
  symbol: String!
  name: String!
  decimals: Int!
  totalSupply: BigInt!
}

type Transfer @entity {
  id: ID!
  token: Token!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  timestamp: BigInt!
  blockNumber: BigInt!
  transactionHash: Bytes!
}

type User @entity {
  id: ID!
  balance: BigInt!
  transfersFrom: [Transfer!]! @derivedFrom(field: "from")
  transfersTo: [Transfer!]! @derivedFrom(field: "to")
}

Subgraph配置(subgraph.yaml)

specVersion: 0.0.4
schema:
  file: ./schema.graphql
dataSources:
  - kind: ethereum
    name: MyToken
    network: mainnet
    source:
      address: "0x..." # 代币合约地址
      abi: ERC20
      startBlock: 12345678
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.6
      language: wasm/assemblyscript
      entities:
        - Token
        - Transfer
        - User
      abis:
        - name: ERC20
          file: ./abis/ERC20.json
      eventHandlers:
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer
      file: ./src/mapping.ts

映射函数(src/mapping.ts)

import { Transfer as TransferEvent } from '../generated/MyToken/ERC20'
import { Token, Transfer, User } from '../generated/schema'
import { BigInt } from '@graphprotocol/graph-ts'

export function handleTransfer(event: TransferEvent): void {
  // 获取或创建Token实体
  let token = Token.load(event.address.toHexString())
  if (token == null) {
    token = new Token(event.address.toHexString())
    token.symbol = 'TOKEN' // 可以调用合约获取
    token.name = 'My Token'
    token.decimals = 18
    token.totalSupply = BigInt.fromI32(0)
  }
  token.save()

  // 创建Transfer实体
  let transfer = new Transfer(
    event.transaction.hash.toHex() + '-' + event.logIndex.toString()
  )
  transfer.token = token.id
  transfer.from = event.params.from
  transfer.to = event.params.to
  transfer.value = event.params.value
  transfer.timestamp = event.block.timestamp
  transfer.blockNumber = event.block.number
  transfer.transactionHash = event.transaction.hash
  transfer.save()

  // 更新From用户余额
  if (event.params.from.toHexString() != '0x0000000000000000000000000000000000000000') {
    let fromUser = User.load(event.params.from.toHexString())
    if (fromUser == null) {
      fromUser = new User(event.params.from.toHexString())
      fromUser.balance = BigInt.fromI32(0)
    }
    fromUser.balance = fromUser.balance.minus(event.params.value)
    fromUser.save()
  }

  // 更新To用户余额
  let toUser = User.load(event.params.to.toHexString())
  if (toUser == null) {
    toUser = new User(event.params.to.toHexString())
    toUser.balance = BigInt.fromI32(0)
  }
  toUser.balance = toUser.balance.plus(event.params.value)
  toUser.save()
}

5.3 查询Subgraph

GraphQL查询

# 查询最近的转账
{
  transfers(
    first: 10,
    orderBy: timestamp,
    orderDirection: desc
  ) {
    id
    from
    to
    value
    timestamp
    blockNumber
  }
}

# 查询用户余额
{
  user(id: "0x...") {
    id
    balance
    transfersFrom(first: 5) {
      to
      value
      timestamp
    }
    transfersTo(first: 5) {
      from
      value
      timestamp
    }
  }
}

# 聚合查询
{
  tokens {
    id
    symbol
    totalSupply
    transfers(first: 1000) {
      value
    }
  }
}

前端集成

import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

// 创建客户端
const client = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/your-subgraph',
  cache: new InMemoryCache()
});

// 查询
async function fetchTransfers() {
  const { data } = await client.query({
    query: gql`
      {
        transfers(first: 10, orderBy: timestamp, orderDirection: desc) {
          id
          from
          to
          value
          timestamp
        }
      }
    `
  });

  return data.transfers;
}

// 订阅(实时更新)
const subscription = client.subscribe({
  query: gql`
    subscription {
      transfers(orderBy: timestamp, orderDirection: desc) {
        id
        from
        to
        value
        timestamp
      }
    }
  `
});

subscription.subscribe({
  next(data) {
    console.log('New transfer:', data);
  }
});

React集成

import { useQuery, gql } from '@apollo/client';

const TRANSFERS_QUERY = gql`
  query GetTransfers($first: Int!, $skip: Int!) {
    transfers(
      first: $first,
      skip: $skip,
      orderBy: timestamp,
      orderDirection: desc
    ) {
      id
      from
      to
      value
      timestamp
    }
  }
`;

function TransferList() {
  const { loading, error, data, fetchMore } = useQuery(TRANSFERS_QUERY, {
    variables: { first: 20, skip: 0 }
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h2>Recent Transfers</h2>
      <table>
        <thead>
          <tr>
            <th>From</th>
            <th>To</th>
            <th>Value</th>
            <th>Time</th>
          </tr>
        </thead>
        <tbody>
          {data.transfers.map(transfer => (
            <tr key={transfer.id}>
              <td>{transfer.from.slice(0, 6)}...</td>
              <td>{transfer.to.slice(0, 6)}...</td>
              <td>{ethers.formatEther(transfer.value)}</td>
              <td>{new Date(transfer.timestamp * 1000).toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <button
        onClick={() =>
          fetchMore({
            variables: { skip: data.transfers.length }
          })
        }
      >
        Load More
      </button>
    </div>
  );
}

六、Chainlink Functions

6.1 链下计算

Chainlink Functions允许智能合约执行链下计算并将结果返回到链上。

应用场景

 复杂数学计算
 机器学习推理
 访问私有API
 数据聚合和处理

6.2 使用Chainlink Functions

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/functions/FunctionsClient.sol";
import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol";

contract WeatherInsurance is FunctionsClient, ConfirmedOwner {
    using Functions for Functions.Request;

    bytes32 public latestRequestId;
    bytes public latestResponse;
    bytes public latestError;

    event ResponseReceived(bytes32 indexed requestId, bytes response, bytes err);

    constructor(address oracle) FunctionsClient(oracle) ConfirmedOwner(msg.sender) {}

    // JavaScript代码(在Chainlink节点上执行)
    string constant source =
        "const location = args[0];"
        "const apiKey = secrets.apiKey;"
        "const url = `https://api.weather.com/v3/wx/conditions/current?location=${location}&apiKey=${apiKey}`;"
        "const response = await Functions.makeHttpRequest({ url });"
        "if (response.error) { throw Error('Request failed'); }"
        "const temp = response.data.temperature;"
        "return Functions.encodeUint256(temp * 100);"; // 返回温度*100

    // 发起请求
    function requestWeatherData(
        string memory location,
        uint64 subscriptionId,
        uint32 gasLimit
    ) public onlyOwner returns (bytes32) {
        Functions.Request memory req;
        req.initializeRequestForInlineJavaScript(source);

        string[] memory args = new string[](1);
        args[0] = location;
        req.setArgs(args);

        latestRequestId = _sendRequest(
            req.encodeCBOR(),
            subscriptionId,
            gasLimit,
            0x66756e2d6574682d6d61696e6e65742d31000000000000 // jobId
        );

        return latestRequestId;
    }

    // 接收响应
    function fulfillRequest(
        bytes32 requestId,
        bytes memory response,
        bytes memory err
    ) internal override {
        latestResponse = response;
        latestError = err;

        emit ResponseReceived(requestId, response, err);

        // 解码温度
        uint256 temp = abi.decode(response, (uint256));

        // 如果温度低于0度,触发保险赔付
        if (temp < 0) {
            // 执行赔付逻辑
            payInsurance();
        }
    }

    function payInsurance() internal {
        // 赔付逻辑
    }
}

七、实战项目:价格预言机DApp

7.1 合约部分

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceBet {
    AggregatorV3Interface public priceFeed;

    struct Bet {
        address player;
        uint256 amount;
        int targetPrice;
        uint256 deadline;
        bool settled;
        bool won;
    }

    Bet[] public bets;

    event BetPlaced(
        uint256 indexed betId,
        address indexed player,
        int targetPrice,
        uint256 amount,
        uint256 deadline
    );

    event BetSettled(uint256 indexed betId, bool won);

    constructor(address _priceFeed) {
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    // 下注: 预测ETH价格
    function placeBet(int targetPrice, uint256 durationInHours) external payable {
        require(msg.value > 0, "Bet amount must be positive");
        require(durationInHours > 0 && durationInHours <= 24, "Invalid duration");

        uint256 deadline = block.timestamp + durationInHours * 1 hours;

        bets.push(Bet({
            player: msg.sender,
            amount: msg.value,
            targetPrice: targetPrice,
            deadline: deadline,
            settled: false,
            won: false
        }));

        emit BetPlaced(bets.length - 1, msg.sender, targetPrice, msg.value, deadline);
    }

    // 结算赌注
    function settleBet(uint256 betId) external {
        Bet storage bet = bets[betId];
        require(!bet.settled, "Already settled");
        require(block.timestamp >= bet.deadline, "Bet not expired");

        bet.settled = true;

        // 获取当前价格
        (, int price, , ,) = priceFeed.latestRoundData();

        // 计算误差(百分比)
        int diff = price > bet.targetPrice ? price - bet.targetPrice : bet.targetPrice - price;
        int errorPercent = diff * 100 / price;

        // 如果误差 <= 5%, 赢得2倍奖励
        if (errorPercent <= 5) {
            bet.won = true;
            payable(bet.player).transfer(bet.amount * 2);
        }

        emit BetSettled(betId, bet.won);
    }

    // 查询当前价格
    function getCurrentPrice() external view returns (int) {
        (, int price, , ,) = priceFeed.latestRoundData();
        return price;
    }

    // 查询用户的所有赌注
    function getUserBets(address user) external view returns (uint256[] memory) {
        uint256 count = 0;
        for (uint i = 0; i < bets.length; i++) {
            if (bets[i].player == user) {
                count++;
            }
        }

        uint256[] memory userBetIds = new uint256[](count);
        uint256 index = 0;
        for (uint i = 0; i < bets.length; i++) {
            if (bets[i].player == user) {
                userBetIds[index] = i;
                index++;
            }
        }

        return userBetIds;
    }
}

7.2 前端部分

// PriceBet.jsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { useWallet } from '../hooks/useWallet';
import { useContract } from '../hooks/useContract';

const PRICE_BET_ADDRESS = '0x...';
const PRICE_BET_ABI = [...];

function PriceBet() {
    const { signer, address } = useWallet();
    const contract = useContract(PRICE_BET_ADDRESS, PRICE_BET_ABI, signer);

    const [currentPrice, setCurrentPrice] = useState('0');
    const [targetPrice, setTargetPrice] = useState('');
    const [betAmount, setBetAmount] = useState('');
    const [duration, setDuration] = useState(1);
    const [myBets, setMyBets] = useState([]);

    // 获取当前价格
    useEffect(() => {
        if (!contract) return;

        async function fetchPrice() {
            const price = await contract.getCurrentPrice();
            setCurrentPrice(ethers.formatUnits(price, 8)); // Chainlink价格有8位小数
        }

        fetchPrice();

        const interval = setInterval(fetchPrice, 10000); // 每10秒更新
        return () => clearInterval(interval);
    }, [contract]);

    // 获取我的赌注
    useEffect(() => {
        if (!contract || !address) return;

        async function fetchMyBets() {
            const betIds = await contract.getUserBets(address);
            const bets = await Promise.all(
                betIds.map(id => contract.bets(id))
            );
            setMyBets(bets.map((bet, index) => ({ ...bet, id: betIds[index] })));
        }

        fetchMyBets();

        // 监听BetPlaced事件
        const filter = contract.filters.BetPlaced(null, address);
        contract.on(filter, fetchMyBets);

        return () => {
            contract.off(filter, fetchMyBets);
        };
    }, [contract, address]);

    // 下注
    const handlePlaceBet = async () => {
        if (!targetPrice || !betAmount) {
            alert('Please fill in all fields');
            return;
        }

        const targetPriceInWei = ethers.parseUnits(targetPrice, 8);
        const betAmountInWei = ethers.parseEther(betAmount);

        const tx = await contract.placeBet(targetPriceInWei, duration, {
            value: betAmountInWei
        });

        await tx.wait();
        alert('Bet placed!');
        setTargetPrice('');
        setBetAmount('');
    };

    // 结算
    const handleSettle = async (betId) => {
        const tx = await contract.settleBet(betId);
        await tx.wait();
        alert('Bet settled!');
    };

    return (
        <div className="price-bet">
            <h2>ETH Price Prediction</h2>

            {/* 当前价格 */}
            <div className="current-price">
                <h3>Current Price</h3>
                <p className="price">${parseFloat(currentPrice).toFixed(2)}</p>
            </div>

            {/* 下注表单 */}
            <div className="bet-form">
                <h3>Place a Bet</h3>
                <input
                    type="number"
                    placeholder="Target Price (USD)"
                    value={targetPrice}
                    onChange={(e) => setTargetPrice(e.target.value)}
                />
                <input
                    type="number"
                    placeholder="Bet Amount (ETH)"
                    value={betAmount}
                    onChange={(e) => setBetAmount(e.target.value)}
                />
                <select value={duration} onChange={(e) => setDuration(e.target.value)}>
                    <option value={1}>1 Hour</option>
                    <option value={6}>6 Hours</option>
                    <option value={12}>12 Hours</option>
                    <option value={24}>24 Hours</option>
                </select>
                <button onClick={handlePlaceBet}>Place Bet</button>
            </div>

            {/* 我的赌注 */}
            <div className="my-bets">
                <h3>My Bets</h3>
                {myBets.length === 0 ? (
                    <p>No bets yet</p>
                ) : (
                    <table>
                        <thead>
                            <tr>
                                <th>Target Price</th>
                                <th>Amount</th>
                                <th>Deadline</th>
                                <th>Status</th>
                                <th>Action</th>
                            </tr>
                        </thead>
                        <tbody>
                            {myBets.map(bet => (
                                <tr key={bet.id.toString()}>
                                    <td>${ethers.formatUnits(bet.targetPrice, 8)}</td>
                                    <td>{ethers.formatEther(bet.amount)} ETH</td>
                                    <td>{new Date(bet.deadline * 1000).toLocaleString()}</td>
                                    <td>
                                        {bet.settled ? (bet.won ? ' Won' : ' Lost') : '⏳ Pending'}
                                    </td>
                                    <td>
                                        {!bet.settled && Date.now() >= bet.deadline * 1000 && (
                                            <button onClick={() => handleSettle(bet.id)}>
                                                Settle
                                            </button>
                                        )}
                                    </td>
                                </tr>
                            ))}
                        </tbody>
                    </table>
                )}
            </div>
        </div>
    );
}

export default PriceBet;

八、总结

核心要点

  1. 预言机是区块链与现实世界的桥梁

    • 提供链下数据
    • 生成真随机数
    • 执行自动化任务
    • 进行链下计算
  2. Chainlink是最成熟的预言机方案

    • Data Feeds: 价格、汇率等数据
    • VRF: 可验证随机数
    • Automation: 自动化任务执行
    • Functions: 链下计算
  3. The Graph简化数据查询

    • 索引区块链事件
    • GraphQL查询接口
    • 实时订阅
    • 复杂聚合查询
  4. 安全考虑

    • 使用多个数据源
    • 检查数据新鲜度
    • 验证价格偏差
    • 处理异常情况

实战建议

  1. 优先使用成熟的预言机服务
  2. 不要自建中心化预言机
  3. 检查数据更新频率和延迟
  4. 考虑预言机费用
  5. 测试网充分测试
  6. 准备备用数据源

下一步学习

继续深入学习更多Web3技术,构建完整的去中心化应用。

Prev
Web3前端开发
Next
智能合约测试与部署