本文详细介绍了如何在 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 网络交互所需的依赖项。我们还将使用 Anchor 安装中包含的 @solana/web3.js。
yarn add @switchboard-xyz/on-demand @switchboard-xyz/common
我们的脚本执行 5 个步骤:
SOL/USD 价格:一个从 Pyth 的链上预言机读取,三个从 REST API 读取。maxStaleness、minimumSamples 等)配置 Feed。import { PublicKey } from "@solana/web3.js";
import * as sb from "@switchboard-xyz/on-demand";
import { AnchorUtils, PullFeed } from "@switchboard-xyz/on-demand";
import { CrossbarClient, decodeString } from "@switchboard-xyz/common";
import {
buildCoinbaseJob,
buildBinanceJob,
buildPythJob,
buildBybitJob,
TX_CONFIG,
} from "./utils";
const crossbarClient = new CrossbarClient(
"https://crossbar.switchboard.xyz",
/* verbose= */ true
);
// 1. 定义四个 FEED JOB 以读取 SOL/USD 价格
const FEED_JOBS = [
// Pyth 预言机传递 SOL/USD 价格 Feed 公钥
buildPythJob("H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG"),
// Web API 端点
buildCoinbaseJob("SOL-USD"),
buildBinanceJob("SOLUSDT"),
buildBybitJob("SOLUSDT"),
];
(async function main() {
// 从 Solana CLI 配置 (~/.config/solana/id.json) 加载钱包和 RPC 连接
// 然后获取网络的默认预言机队列
const { keypair, connection, program } = await AnchorUtils.loadEnv();
const queueAccount = await sb.getDefaultQueue(connection.rpcEndpoint);
const queue = queueAccount.pubkey;
// 2. FEED 配置
const conf: any = {
name: "SOL-USD Price Feed", // feed 名称 (最大 32 字节)
queue: new PublicKey(queue), // 绑定的预言机队列
maxVariance: 1.0, // 允许提交和 Jobs 之间 1% 的方差
minResponses: 3, // 允许的 Jobs 响应的最小数量
numSignatures: 3, // 每次更新获取的签名数量
minSampleSize: 3, // 采样结果的最小响应数量
maxStaleness: 100, // 采样响应的最大陈旧 slots 数量
};
// 3. 生成 FEED 密钥对
console.log("正在初始化新的数据 Feed");
const [pullFeed, feedKp] = PullFeed.generate(program!);
// 4. 将 JOB 定义存储在 IPFS 上
conf.feedHash = decodeString(
(await crossbarClient.store(queue.toString(), FEED_JOBS)).feedHash
);
// 5. 构建并发送初始化交易
// 包含 FEED 配置
const initTx = await sb.asV0Tx({
connection,
ixs: [await pullFeed.initIx(conf)],
payer: keypair.publicKey,
signers: [keypair, feedKp],
computeUnitPrice: 75_000,
computeUnitLimitMultiple: 1.3,
});
console.log("正在发送初始化交易");
const sig = await connection.sendTransaction(initTx, TX_CONFIG);
await connection.confirmTransaction(sig, "confirmed");
console.log(`Feed ${feedKp.publicKey} 已初始化: ${sig}`);
})();
让我们解释一下上述代码中 Switchboard 特定的关键部分:
Oracle queues
脚本使用 sb.getDefaultQueue() 检索你网络的默认预言机队列。队列是 Switchboard 用于协调预言机节点的机制。当你将 Feed 绑定到队列时,你是在告诉 Switchboard 网络哪个预言机池可以满足你的 Feed 的更新请求。每个队列都有自己注册的预言机集、奖励参数和操作规则。在 Devnet 上,这会返回所有开发者共享的公共测试队列。
Feed 配置
在上述代码中,Feed 配置是用于将 Jobs 结果聚合成一个单一、可信值的规则集。这种信任来自我们配置中的多层验证:
minResponses:我们要求每次更新至少有 3 个成功的响应,并且在计算结果时至少采样 3 个提交。这与我们链上调用 get_value 中的 min_samples = 3 相符。maxVariance:我们将其设置为 1.0,这意味着如果一个源报告的价格与其他源相差超过 1%,则可以将其视为不一致而丢弃。maxStaleness:这确保了数据的时效性,对应于我们链上程序中的 max_stale_slots 值。使用我们之前安装的 JavaScript 运行时 bun 运行脚本:
bun run scripts/initializeFeeds.ts
结果将包含程序中 Feed 账户的公钥,如下所示:

这个 Feed 现在在 Devnet 上是实时的,但它仍然是空的。在下一节中,我们将使用这个公钥开始用数据填充它。
现在我们的 Feed 账户已初始化,我们需要一个过程来持续用新数据填充它。Feed 更新是确保 Feed 账户始终包含最新数据的过程。
我们将创建一个脚本 runfeeds.ts,它运行一个无限循环。在每次迭代中,它使用 Feed 公钥从 Switchboard 网络请求我们 Jobs 的最新价格。来自所有预言机的数据被聚合,然后脚本发送一个交易,将经过验证的结果存储到我们的链上 Feed 账户中。
import * as sb from "@switchboard-xyz/on-demand";
import { CrossbarClient } from "@switchboard-xyz/common";
import yargs from "yargs";
import { TX_CONFIG, sleep } from "./utils";
import { PublicKey } from "@solana/web3.js";
/// 解析命令行参数 - 需要 Feed 账户公钥 '--feed'
const argv = yargs(process.argv).options({ feed: { required: true } })
.argv as any;
console.log(`正在使用 Feed: ${argv.feed}`);
// 主函数被包裹在一个立即执行的异步函数表达式 (IIFE) 中。
(async function main() {
// 从本地环境加载钱包密钥对、RPC 连接和程序
const { keypair, connection, program } = await sb.AnchorUtils.loadEnv();
// 为指定网络(devnet 或 mainnet)加载默认的 Switchboard 队列账户。
const queue = await sb.Queue.loadDefault(program!);
// 初始化 'PullFeed' 对象以与
// 终端中指定的链上数据 Feed 公钥进行交互。
const feedAccount = new sb.PullFeed(program!, argv.feed!);
// 连接到 Switchboard 的链下基础设施
const crossbar = new CrossbarClient("https://crossbar.switchboard.xyz");
const gateway = await queue.fetchGatewayFromCrossbar(crossbar as any);
// 缓存地址查找表以减少交易大小
await feedAccount.preHeatLuts();
let runCount = 0;
console.log("正在启动 Feed 更新程序。");
// 启动一个无限循环以持续更新数据 Feed。
while (true) {
try {
console.log(`\n--- 更新 #${++runCount} ---`);
// 请求新的预言机数据。网关从 Feed 账户读取 feedHash,
// 从 IPFS 检索 Job 定义,将其分发给预言机,并返回
// 它们的聚合响应
const [pullIx, responses, _ok, luts] = await feedAccount.fetchUpdateIx({
gateway: gateway.gatewayUrl,
crossbarClient: crossbar as any,
});
// 检查预言机错误
let hasError = false;
for (const response of responses) {
const shortErr = response.shortError();
if (shortErr) {
console.log(`预言机响应错误: ${shortErr}`);
hasError = true;
}
}
// 如果发生错误或没有返回指令,则跳过更新
if (hasError || !pullIx || pullIx.length === 0) {
console.log("由于预言机错误或未生成指令,跳过更新。");
await sleep(5000); // 如果有错误,等待更长时间再重试。
continue;
}
// 将更新指令组装成一个版本化交易 (v0)。
const tx = await sb.asV0Tx({
connection,
ixs: [...pullIx!],
signers: [keypair], // 支付者的密钥对必须签署交易。
computeUnitPrice: 200_000, // 设置优先级费用以更快地处理交易。
computeUnitLimitMultiple: 1.3, // 为计算单元限制添加缓冲区以防止失败。
lookupTables: luts, // 包含预热的 LUT。
});
// 将交易发送到链上以更新 Feed
const sig = await connection.sendTransaction(tx, TX_CONFIG);
console.log(`✅ 交易已发送: https://explorer.solana.com/tx/${sig}?cluster=devnet`);
console.log("等待确认...");
await connection.confirmTransaction(sig, "confirmed");
console.log("✅ 交易已确认!");
} catch (e) {
console.error("❌ 主循环中发生错误:", e);
} finally {
// 在开始下一个更新周期之前暂停执行几秒钟。
await sleep(5000);
}
}
})();
IPFS 内容哈希的用法
当调用 feedAccount.fetchUpdateIx() 时:
feedHash(初始化期间存储的 IPFS 内容哈希)。feedHash 的请求。feedHash 从 IPFS 检索 Job 定义。fetchUpdateIx() 返回将聚合的预言机数据写入 Feed 账户的指令 (pullIx)。我们将使用 Feed 公钥作为命令行参数来运行脚本,如下所示:
bun scripts/runfeeds.ts --feed GgGVgSLWAyL9Xf4fGaAQQCkmWetBjX7PCNz8kTK97DKB
结果将如下所示:

我们将让这个脚本持续运行,以确保我们始终接收到最新数据。
现在程序已部署,Feed 也在持续更新,让我们编写一个脚本来读取 SOL/USD 的价格。
以下脚本执行以下操作:
GgGVgSLWAyL9Xf4fGaAQQCkmWetBjX7PCNz8kTK97DKB) 提供创建的 Feed 账户公钥。.readPrice() 方法指的是链上的 read_price 方法。.accounts({ feed }) 步骤将 Feed 账户绑定到指令。msg! 输出会在此处显示,脚本会捕获并显示它。import { PublicKey } from "@solana/web3.js";
import * as sb from "@switchboard-xyz/on-demand";
import * as anchor from "@coral-xyz/anchor";
import { TX_CONFIG } from "./utils";
(async function main() {
try {
console.log("正在从 Switchboard 获取 SOL/USD 价格...");
// 从本地环境加载密钥对和连接
const { keypair, connection } = await sb.AnchorUtils.loadEnv();
// 创建 Anchor Provider 并附加上去
const provider = new anchor.AnchorProvider(
connection,
new anchor.Wallet(keypair),
{ commitment: "confirmed" }
);
anchor.setProvider(provider);
// 使用 Anchor 的工作区加载已部署的程序
const program = anchor.workspace.switchOnDemandPriceFeed;
// ====== 1. 定义 FEED 账户 ======
// 替换为你的 Feed 账户地址,来自 initializeFeeds.ts 的输出
const feedAccount = new PublicKey("GgGVgSLWAyL9Xf4fGaAQQCkmWetBjX7PCNz8kTK97DKB");
// 2. **构建指令**
// 构建指令以调用链上的 'read_price' 方法
const ix = await program.methods
.readPrice()
.accounts({
feed: feedAccount,
})
.instruction();
// ====== 3. 构建交易 ======
// 包装在一个版本化交易中,由支付者签名
const tx = await sb.asV0Tx({
connection,
ixs: [ix],
payer: keypair.publicKey,
signers: [keypair],
computeUnitPrice: 200_000,
computeUnitLimitMultiple: 1.3,
});
// ====== 4. 交易模拟 ======
// 模拟交易以捕获日志(包括我们的价格输出)
const sim = await connection.simulateTransaction(tx, TX_CONFIG);
if (sim.value.logs) {
const priceLog = sim.value.logs.find(log =>
log.includes("SOL/USD price:"));
if (priceLog) {
console.log(`✅ ${priceLog}`);
} else {
console.log("所有日志:", sim.value.logs);
}
}
// ====== 5. 将交易发送到 Solana 网络 ======
// 发送实际交易并记录其签名链接
const sig = await connection.sendTransaction(tx, TX_CONFIG);
console.log(`📝 交易: https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.error("❌ 获取价格时出错:", error);
}
})();
现在,在一个终端中激活 runfeeds.ts,打开第二个终端并运行读取脚本:
bun run scripts/showPrice.ts
你应该会看到直接从你的链上程序记录的 SOL/USD 价格,这证实了整个数据管道正在正常工作。

在我们的示例中,我们只是获取价格并显示它。你可以构建任何使用 Switchboard 链上数据的应用程序。
我们学习了如何与 Switchboard 预言机工作流交互,以从多个来源获取可靠数据并在链上使用。此过程涉及的步骤是:
创建一个程序来计算价格影响,评估交易滑点将如何影响交易。该函数应利用 Switchboard 价格 Feed 数据。
- 原文链接: rareskills.io/post/solan...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!