Switchboard
Switchboardは、開発者が価格フィード、NFT 最低価格、スポーツ統計、さらには検証可能なランダム性など、さまざまなユースケースのためにチェーン上でデータを調達できるようにする Oracle プロトコルです。 一般的な意味で、Switchboardはオフチェーンのリソースであり、開発者はチェーン上で整合性の高いデータを橋渡しし、次世代の web3とDeFiを強化するために呼び出すことができます。
Data Feeds
Switchboardは @switchboard-xyz/switchboard-v2 として呼び出されるJavaScript/TypeScriptライブラリです。このライブラリを使用して、既存のデータフィードからオンチェーン データにアクセスしたり、独自のカスタムフィードを公開したりできます。詳細はこちら
アグリゲーターフィードからデータを読み取る
import {
clusterApiUrl,
Connection,
Keypair,
PublicKey,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as switchboard from "@switchboard-xyz/switchboard-v2";
async () => {
const payer = Keypair.generate();
const connection = new Connection(clusterApiUrl("devnet"));
const program = await switchboard.loadSwitchboardProgram(
"devnet",
connection,
payer
);
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
console.log("here");
const aggregatorAccount = new switchboard.AggregatorAccount({
program: program,
publicKey: aggregatorKey,
});
const result: any = await aggregatorAccount.getLatestValue();
console.log(result.toNumber());
};
let aggregatorKey: PublicKey;
let program: Program<Idl>;
const aggregatorAccount = new switchboard.AggregatorAccount({
program: program,
publicKey: aggregatorKey,
});
const result: any = await aggregatorAccount.getLatestValue();
アグリゲーターフィードを新規作成する
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
AggregatorAccount,
loadSwitchboardProgram,
LeaseAccount,
OracleQueueAccount,
SwitchboardPermission,
} from "@switchboard-xyz/switchboard-v2";
let payer: Keypair;
let authority: Keypair; // queue authority
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const queueAccount = new OracleQueueAccount({
program,
publicKey: queuePubkey,
});
// aggregator
const aggregatorAccount = await AggregatorAccount.create(program, {
name: Buffer.from("MY SOL/USD Feed"),
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 10,
queueAccount,
authority: authority.publicKey,
});
// permission
const permissionAccount = await PermissionAccount.create(program, {
authority: authority.publicKey,
granter: queueAccount.publicKey,
grantee: aggregatorAccount.publicKey,
});
await aggregatorPermission.set({
authority,
permission: SwitchboardPermission.PERMIT_ORACLE_QUEUE_USAGE,
enable: true,
});
// lease
const leaseContract = await LeaseAccount.create(program, {
loadAmount: new anchor.BN(0),
funder: tokenAccount,
funderAuthority: authority,
oracleQueueAccount: queueAccount,
aggregatorAccount,
});
// job
const tasks: OracleJob.Task[] = [
OracleJob.Task.create({
httpTask: OracleJob.HttpTask.create({
url: `https://ftx.us/api/markets/SOL_USD`,
}),
}),
OracleJob.Task.create({
jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.result.price" }),
}),
];
const jobData = Buffer.from(
OracleJob.encodeDelimited(
OracleJob.create({
tasks,
})
).finish()
);
const jobKeypair = anchor.web3.Keypair.generate();
const jobAccount = await JobAccount.create(program, {
data: jobData,
keypair: jobKeypair,
authority: authority.publicKey,
});
// add job to aggregator
await aggregatorAccount.addJob(jobAccount, authority);
const queueAccount = new OracleQueueAccount({
program,
publicKey: queuePubkey,
});
const aggregatorAccount = await AggregatorAccount.create(program, {
name: Buffer.from("MY SOL/USD Feed"),
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 10,
queueAccount,
authority: authority.publicKey,
});
console.log(aggregatorAccount.publicKey.toString());
プログラムでアグリゲーターフィードからデータを読み取る
Switchboardはswitchboard_v2というクレートを提供します。詳細については、こちらをご覧ください。
use anchor_lang::prelude::*;
use switchboard_v2::AggregatorAccountData;
declare_id!("HDa6A4wLjEymb8Cv3UGeGBnFUNCUEMpoiVBbkTKAkfrt");
#[program]
pub mod get_result {
use super::*;
pub fn get_result(ctx: Context<GetResult>) -> Result<()> {
let aggregator = &ctx.accounts.aggregator_feed.load()?;
let val:f64 = aggregator
.get_result()?
.try_into()?;
msg!("Current feed result is {}!", val);
Ok(())
}
}
#[derive(Accounts)]
pub struct GetResult<'info> {
pub authority: Signer<'info>,
/// CHECK: field is unsafe
pub aggregator_feed: AccountLoader<'info, AggregatorAccountData>, // pass aggregator key
}
let aggregator = &ctx.accounts.aggregator_feed.load()?;
let val:f64 = aggregator
.get_result()?
.try_into()?;
パブリッシャーからフィードを作成する方法
公式の Switchboard ドキュメントには、パブリッシャーからフィードを作成する方法の詳細なウォークスルーがあります。こちらをチェックしてください。
Oracle
Switchboardのユニークな機能は、独自のOracleを作成してローカルで実行できることです。
Oracleを作成
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
loadSwitchboardProgram,
OracleAccount,
OracleQueueAccount,
} from "@switchboard-xyz/switchboard-v2";
let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const queueAccount = new OracleQueueAccount({
program,
publicKey: queuePubkey,
});
// Create oracle
const oracleAccount = await OracleAccount.create(program, {
name: Buffer.from("My Oracle"),
queueAccount,
});
const queueAccount = new OracleQueueAccount({
program,
publicKey: queuePubkey,
});
// Create oracle
const oracleAccount = await OracleAccount.create(program, {
name: Buffer.from("My Oracle"),
queueAccount,
});
Oracleをローカルで実行
Oracleをローカルで実行し、独自のOracleキューに割り当てて、プログラムが本番環境でどのように動作するかをテストできます。メインネットのOracleは、監視機能のセットを備えた高可用性環境で常に実行する必要があります。
要件
- Docker-compose
Oracle Configで環境変数を使用してdocker-compose.ymlを作成します。
version: "3.3"
services:
switchboard:
image: "switchboardlabs/node:dev-v2-5-28-22a"
network_mode: host
restart: always
environment:
- LIVE=1
- CLUSTER=devnet
- RPC_URL=${RPC_URL}
- ORACLE_KEY=${ORACLE_KEY}
- HEARTBEAT_INTERVAL=15
volumes:
- ./configs.json:/configs.json
secrets:
PAYER_SECRETS:
file: /filesystem/path/to/keypair.json
version: "3.3"
services:
switchboard:
image: "switchboardlabs/node:dev-v2-5-28-22a"
network_mode: host
restart: always
environment:
- LIVE=1
- CLUSTER=devnet
- RPC_URL=${RPC_URL}
- ORACLE_KEY=${ORACLE_KEY}
- HEARTBEAT_INTERVAL=15
volumes:
- ./configs.json:/configs.json
secrets:
PAYER_SECRETS:
file: /filesystem/path/to/keypair.json
docker-compose up
を使用してコンテナを実行します。
Oracle Config
Env Variable | Definition |
---|---|
ORACLE_KEY | Required Type - Public Key Description - Oracleキューを使用する権限を付与されたOracleアカウントの公開鍵 |
HEARTBEAT_INTERVAL | Optional Type - Number (seconds) Default - 30 Description - Oracle ハートビート間の秒数。キューには、異なるOracleハートビート要件があります。推奨値は15です |
GCP_CONFIG_BUCKET | Optional Type - GCP Resource Path Default - 現在の作業ディレクトリで configs.json を探します。見つからない場合、構成はロードされません。 Description - プライベートAPIエンドポイントのAPIキーを含む |
UNWRAP_STAKE_THRESHOLD | Optional Type - Number (SOL amount, Ex. 1.55) Default - 0, disabled. Description - unwrap stake actionをトリガーする Solana の残高。OracleのSolana 残高が設定されたしきい値を下回ると、ノードは自動的にオラクルのステーキングウォレットから資金をアンラップし、少なくとも0.1 wSOL またはキューの最小ステーク要件より10%多い金額を残します。 |
検証可能な確率関数(VRF)
検証可能な確率関数(Verifiable Random Function/VRF)出力が正しく計算されたことを証明する公開鍵疑似乱数関数です。
VRFアカウントの読み取り
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
loadSwitchboardProgram,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const vrfAccount = new VrfAccount({
program,
publicKey: vrfKey,
});
const vrf = await vrfAccount.loadData();
console.log(vrf.currentRound.result);
const vrfAccount = new VrfAccount({
program,
publicKey: vrfKey,
});
const vrf = await vrfAccount.loadData();
console.log(vrf.currentRound.result);
use switchboard_v2::VrfAccountData;
let vrf = VrfAccountData::new(vrf_account_info)?;
let result_buffer = vrf.get_result()?;
if result_buffer == [0u8; 32] {
msg!("vrf buffer empty");
return Ok(());
}
let value: &[u128] = bytemuck::cast_slice(&result_buffer[..]);
let result = value[0] % 256000 as u128;
let vrf = VrfAccountData::new(vrf_account_info)?;
let result_buffer = vrf.get_result()?;
VRFアカウントの作成
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
loadSwitchboardProgram,
OracleQueueAccount,
PermissionAccount,
SwitchboardPermission,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const queueAccount = new queueAccount({ program, publicKey: queueKey });
const queue = await queueAccount.loadData();
// load client program used for callback
const vrfClientProgram = anchor.workspace
.AnchorVrfParser as anchor.Program<AnchorVrfParser>;
const vrfSecret = anchor.web3.Keypair.generate();
const vrfIxCoder = new anchor.BorshInstructionCoder(vrfClientProgram.idl);
const vrfClientCallback: Callback = {
programId: vrfClientProgram.programId,
accounts: [
// ensure all accounts in updateResult are populated
{ pubkey: vrfClientKey, isSigner: false, isWritable: true },
{ pubkey: vrfSecret.publicKey, isSigner: false, isWritable: false },
],
ixData: vrfIxCoder.encode("updateResult", ""), // pass any params for instruction here
};
// create VRF
const vrfAccount = await VrfAccount.create(program, {
queue: queueAccount,
callback: vrfClientCallback,
authority: vrfClientKey, // vrf authority
keypair: vrfSecret,
});
// create permission
const permissionAccount = await PermissionAccount.create(program, {
authority: queue.authority,
granter: queue.publicKey,
grantee: vrfAccount.publicKey,
});
// if queue has not enabled unpermissionedVrfEnabled, queue will need to grant permission
let queueAuthority: Keypair;
await permissionAccount.set({
authority: queueAuthority,
permission: SwitchboardPermission.PERMIT_VRF_REQUESTS,
enable: true,
});
const vrfAccount = await VrfAccount.create(program, {
queue: queueAccount,
callback: vrfClientCallback,
authority: vrfClientKey, // vrf authority
keypair: vrfSecret,
});
VRFアカウントからRandomnessを要求する
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
loadSwitchboardProgram,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
let payer: Keypair;
let authority: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const vrfAccount = new VrfAccount({
program,
publicKey: vrfKey,
});
const vrf = await vrfAccount.loadData();
const queueAccount = new OracleQueueAccount({
program,
publicKey: vrf.queuePubkey,
});
const queue = await queueAccount.loadData();
const mint = await queueAccount.loadMint();
const payerTokenWallet = (
await mint.getOrCreateAssociatedAccountInfo(payer.publicKey)
).address;
const signature = await vrfAccount.requestRandomness({
authority,
payer: payerTokenWallet,
payerAuthority: payer,
});
const signature = await vrfAccount.requestRandomness({
authority,
payer: payerTokenWallet,
payerAuthority: payer,
});
use crate::*;
use anchor_lang::prelude::*;
pub use switchboard_v2::{VrfAccountData, VrfRequestRandomness};
use anchor_spl::token::Token;
use anchor_lang::solana_program::clock;
#[derive(Accounts)]
#[instruction(params: RequestResultParams)] // rpc parameters hint
pub struct RequestResult<'info> {
#[account(
mut,
seeds = [
STATE_SEED,
vrf.key().as_ref(),
authority.key().as_ref(),
],
bump = state.load()?.bump,
has_one = vrf,
has_one = authority
)]
pub state: AccountLoader<'info, VrfClient>,
#[account(signer)]
pub authority: AccountInfo<'info>,
#[account(constraint = switchboard_program.executable == true)]
pub switchboard_program: AccountInfo<'info>,
#[account(mut, constraint = vrf.owner.as_ref() == switchboard_program.key().as_ref())]
pub vrf: AccountInfo<'info>,
#[account(mut, constraint = oracle_queue.owner.as_ref() == switchboard_program.key().as_ref())]
pub oracle_queue: AccountInfo<'info>,
pub queue_authority: UncheckedAccount<'info>,
#[account(constraint = data_buffer.owner.as_ref() == switchboard_program.key().as_ref())]
pub data_buffer: AccountInfo<'info>,
#[account(mut, constraint = permission.owner.as_ref() == switchboard_program.key().as_ref())]
pub permission: AccountInfo<'info>,
#[account(mut, constraint = escrow.owner == program_state.key())]
pub escrow: Account<'info, TokenAccount>,
#[account(mut, constraint = payer_wallet.owner == payer_authority.key())]
pub payer_wallet: Account<'info, TokenAccount>,
#[account(signer)]
pub payer_authority: AccountInfo<'info>,
#[account(address = solana_program::sysvar::recent_blockhashes::ID)]
pub recent_blockhashes: AccountInfo<'info>,
#[account(constraint = program_state.owner.as_ref() == switchboard_program.key().as_ref())]
pub program_state: AccountInfo<'info>,
#[account(address = anchor_spl::token::ID)]
pub token_program: Program<'info, Token>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct RequestResultParams {
pub permission_bump: u8,
pub switchboard_state_bump: u8,
}
impl RequestResult<'_> {
pub fn validate(&self, _ctx: &Context<Self>, _params: &RequestResultParams) -> Result<()> {
Ok(())
}
pub fn actuate(ctx: &Context<Self>, params: &RequestResultParams) -> Result<()> {
let client_state = ctx.accounts.state.load()?;
let bump = client_state.bump.clone();
let max_result = client_state.max_result.clone();
drop(client_state);
let switchboard_program = ctx.accounts.switchboard_program.to_account_info();
let vrf_request_randomness = VrfRequestRandomness {
authority: ctx.accounts.state.to_account_info(),
vrf: ctx.accounts.vrf.to_account_info(),
oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
queue_authority: ctx.accounts.queue_authority.to_account_info(),
data_buffer: ctx.accounts.data_buffer.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
escrow: ctx.accounts.escrow.clone(),
payer_wallet: ctx.accounts.payer_wallet.clone(),
payer_authority: ctx.accounts.payer_authority.to_account_info(),
recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
program_state: ctx.accounts.program_state.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let vrf_key = ctx.accounts.vrf.key.clone();
let authority_key = ctx.accounts.authority.key.clone();
msg!("bump: {}", bump);
msg!("authority: {}", authority_key);
msg!("vrf: {}", vrf_key);
let state_seeds: &[&[&[u8]]] = &[&[
&STATE_SEED,
vrf_key.as_ref(),
authority_key.as_ref(),
&[bump],
]];
msg!("requesting randomness");
vrf_request_randomness.invoke_signed(
switchboard_program,
params.switchboard_state_bump,
params.permission_bump,
state_seeds,
)?;
emit!(RequestingRandomness{
vrf_client: ctx.accounts.state.key(),
max_result: max_result,
timestamp: clock::Clock::get().unwrap().unix_timestamp
});
msg!("randomness requested successfully");
Ok(())
}
}
let vrf_request_randomness = VrfRequestRandomness {
authority: ctx.accounts.state.to_account_info(),
vrf: ctx.accounts.vrf.to_account_info(),
oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
queue_authority: ctx.accounts.queue_authority.to_account_info(),
data_buffer: ctx.accounts.data_buffer.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
escrow: ctx.accounts.escrow.clone(),
payer_wallet: ctx.accounts.payer_wallet.clone(),
payer_authority: ctx.accounts.payer_authority.to_account_info(),
recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
program_state: ctx.accounts.program_state.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let vrf_key = ctx.accounts.vrf.key.clone();
let authority_key = ctx.accounts.authority.key.clone();
msg!("bump: {}", bump);
msg!("authority: {}", authority_key);
msg!("vrf: {}", vrf_key);
let state_seeds: &[&[&[u8]]] = &[&[
&STATE_SEED,
vrf_key.as_ref(),
authority_key.as_ref(),
&[bump],
]];
msg!("requesting randomness");
vrf_request_randomness.invoke_signed(
switchboard_program,
params.switchboard_state_bump,
params.permission_bump,
state_seeds,
)?;