链下数据与预言机
章节导读
智能合约运行在封闭的区块链环境中,无法主动访问外部世界的数据。这就是所谓的"预言机问题"(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;
八、总结
核心要点
预言机是区块链与现实世界的桥梁
- 提供链下数据
- 生成真随机数
- 执行自动化任务
- 进行链下计算
Chainlink是最成熟的预言机方案
- Data Feeds: 价格、汇率等数据
- VRF: 可验证随机数
- Automation: 自动化任务执行
- Functions: 链下计算
The Graph简化数据查询
- 索引区块链事件
- GraphQL查询接口
- 实时订阅
- 复杂聚合查询
安全考虑
- 使用多个数据源
- 检查数据新鲜度
- 验证价格偏差
- 处理异常情况
实战建议
- 优先使用成熟的预言机服务
- 不要自建中心化预言机
- 检查数据更新频率和延迟
- 考虑预言机费用
- 测试网充分测试
- 准备备用数据源
下一步学习
继续深入学习更多Web3技术,构建完整的去中心化应用。