本文详细介绍了如何在 Solana 区块链上使用去中心化预言机网络 Switchboard 来获取链下数据,特别是 SOL/USD 的价格。它涵盖了 Solana 智能合约的编写、Switchboard 价格喂价的初始化与配置、以及客户端脚本如何更新和读取链上价格数据。
链上程序无法直接访问链下数据。它们依赖预言机获取资产价格、事件结果或 API 响应等信息。没有这些预言机,程序的范围将仅限于已存储在链上的状态。
Switchboard 是一个多链去中心化预言机网络,最初构建于 Solana 之上,旨在为智能合约提供可靠的链下数据,例如价格、天气和事件数据。在本教程中,我们将构建一个 Solana 程序,该程序从 Switchboard 读取当前的 SOL/USD 价格。
本教程将完成三件事:
在我们构建 Solana 程序之前,让我们先了解 Switchboard 的工作原理。
Switchboard 使用以下 4 个关键组件来允许 Solana 程序读取链下数据:
i128 值提交到链上的 Feed。Decimal 类型以进行十进制运算。总而言之,以上组件的工作原理如下:
i128 值。在本教程中,我们将学习这些过程是如何工作的。让我们开始实施价格源。
要跟进本教程,你需要一个正常运行的 Solana 开发环境,并安装了以下工具:
curl -fsSL https://bun.sh/install | bash 命令来安装 bun。我们将使用独立的脚本而不是单元测试来与已部署的程序进行交互。这是因为价格源会在 Devnet 上更新,我们希望展示实时预言机数据如何流入链上逻辑。
通过在终端运行以下命令,将 Solana 集群设置为 Devnet:
solana config set --url https://api.devnet.solana.com
你还需要一些 Devnet 上的 SOL 来支付交易费用。你可以使用 solana airdrop 从水龙头请求测试 SOL:
solana airdrop 2 # 请求 2 个 Devnet SOL。
你每次只能在 Devnet 水龙头请求 2 个 SOL。你可以先请求 2 个,等待片刻后再请求另一个,因为它有速率限制。
在 Solana 中,时间以 slot 为单位衡量,slot 是网络时间中链条前进的顺序间隔;slot 号随着网络进程而增加,并被用作链上事件排序的简单时钟。这与以太坊的 Block number (block.number) 相似,仅因为它们都代表时间的推移和事件的排序。
请记住 slot 这个概念,Switchboard 用它来衡量数据的陈旧性,我们将在本文后面看到它的用法。
我们将首先创建一个 Anchor 项目,该项目定义将从 Switchboard 拉取价格数据的 Solana 程序。
anchor init switchboard-demo
cd switchboard-demo
更新你的 Anchor.toml 提供程序中的 cluster 字段以使用 Devnet,因为我们将在 Devnet 上工作:
[provider]
cluster = "Devnet"
wallet = "~/.config/solana/id.json"
接下来,我们将 switchboard-on-demand crate 添加到 programs/switchboard-demon/src/Cargo.toml 文件的依赖项部分。这是我们将使用的 Switchboard crate。
[dependencies]
anchor-lang = "0.31.1"
switchboard-on-demand = "0.5.3"
在 programs/switchboard-demo/src/lib.rs 内部,我们编写一个程序,它:
switchboard-on-demand crate 中的 PullFeedAccountData 结构。get_value 方法,并传入以下参数以验证和提取最新价格:
max_stale_slots:设置自 Feed 账户上次更新以来的 Solana slot 最大数量。如果 Feed 比此值更旧,feed.get_value 将失败。min_samples:设置有效价格所需的预言机提交的最小数量。only_positive:当为 true 时,拒绝非正值(≤ 0)。适用于价格或数量必须始终为正的情况。msg! 记录 SOL/USD 价格。use anchor_lang::prelude::*;
use switchboard_on_demand::{
on_demand::accounts::pull_feed::PullFeedAccountData,
prelude::rust_decimal,
};
use rust_decimal::Decimal;
declare_id!("iSYBH57FJPsqKnVxz8pyqPvCLEBH63y95Vgk346utR2");
#[program]
pub mod switch_on_demand_price_feed {
use super::*;
pub fn read_price(ctx: Context<ReadPrice>) -> Result<()> {
// 步骤 1:读取 FEED 账户的原始二进制数据(字节)
let data_slice = ctx.accounts.feed.data.borrow();
// 步骤 2:解析 FEED 账户数据
let feed = PullFeedAccountData::parse(data_slice).unwrap();
// 步骤 3:检索 FEED 值,带有 SLOT、采样约束,
// 以及确定接收值是正数还是负数的参数
let price: Decimal = feed.get_value(
&Clock::get()?,
/*max_stale_slots=*/ 100,
/*min_samples=*/ 3,
/*only_positive=*/ true,
).unwrap();
// 步骤 4:使用 `msg!` 记录 SOL/USD 价格。
msg!("SOL/USD price: {}", price);
Ok(())
}
}
#[derive(Accounts)]
pub struct ReadPrice<'info> {
/// CHECK: 这是一个 Switchboard 链上 Feed (PullFeedAccount)
pub feed: AccountInfo<'info>,
}
请注意第 3 步中关于 slot 陈旧性和采样约束的注释。每次 Switchboard 在链上写入新值时,它都会记录 slot 号。当你调用 feed.get_value(&Clock::get()?, max_stale_slots, ...) 时,Switchboard 会将当前 slot 与 Feed 的上次更新 slot 进行比较。
如果差异超过 max_stale_slots(在我们的代码中为 100),get_value 将返回错误。指令将失败,交易将被拒绝。
此外,min_samples 参数确保我们聚合了足够多的预言机响应以提高准确性。在我们的示例中,我们将其设置为 3,这意味着结果必须包含至少 3 个预言机响应的数据。在本文后面讨论链下初始化时,我们将看到如何配置这些预言机。
接下来,通过运行以下命令构建并部署程序:
anchor build && anchor deploy
成功部署将返回程序 Id 和签名,如下所示:

我们的链上程序现已部署,可以记录 SOL/USD 的价格,但我们还不能使用它,因为虽然程序已准备好从 feed 账户读取价格,但它还没有一个特定的 Feed 可供读取。接下来,我们将通过设置链下 Feed 来解决这个问题。
回想一下,Feed 是存储预言机提交结果的链上账户。
设置 Feed 涉及两个步骤,我们将在接下来介绍:
Feed 配置定义了你的预言机的数据源和聚合规则(例如链上的 max_stale_slots)。你指定要查询哪些外部 API 或链上预言机、需要多少响应以及源之间可接受的差异。此配置在任何数据上链之前以 JavaScript 对象形式存在。
Feed 初始化将配置转换为实际的链上账户。初始化交易存储 Feed 的元数据,将其绑定到预言机节点池(在 Switchboard 中称为“oracle queue”),并生成程序在请求价格数据时必须引用的公钥。
与 Chainlink 预言机不同,Switchboard Feed 不会自动更新。它使用拉取模型。你的程序必须通过链下脚本触发一个 Job 来获取新数据并更新链上 Feed。
每个 Job 都包含:
httpTask:从 URL 获取 Feed 数据。jsonParseTask:使用路径查询从 httpTask 返回的 JSON 响应中提取特定值。以下示例展示了一个包含两个任务的 Job:一个从 Coinbase API 获取汇率数据,另一个解析响应。

通常,你会自己实现这些 Jobs,每个 Job 使用一个函数。但是,为了简单起见,在此示例中我们不会手动实现获取逻辑。
Switchboard 团队已经提供了一个公共的 utils.ts 文件,其中包含常见的 Job 获取实现。我们将改用这些。打开 GitHub 上的 utils 文件,复制内容,并将其粘贴到 /scripts/utils.ts 中。
设置多个数据源
上图显示了一个只有一个源的任务。在真实程序中,你需要多个源,这意味着你将有多个 Jobs。多个源可以防止单点故障,并让 Feed 通过方差检查过滤掉异常值。
创建初始化脚本
在 /scripts 文件夹中创建一个 initializeFeeds.ts 文件,并运行以下命令来安装与 Switchboard 网络交...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!