Switchboard
Switchboard adalah protokol Oracle yang memungkinkan pengembang untuk menarik data on-chain untuk berbagai kasus penggunaan seperti feed harga, harga dasar NFT, statistik olahraga, atau bahkan keacakan yang dapat diverifikasi. Secara umum, Switchboard adalah sumber daya off-chain yang dapat digunakan untuk menjembatani data dengan integritas tinggi secara on-chain dan memberi daya pada web3 dan DeFi.
Data Feeds
Switchboard menyediakan librari JavaScript/TypeScript yang disebut @switchboard-xyz/switchboard-v2 . Librari ini bisa digunakan untuk mendapatkan on-chain data dari feeds yang sudah ada atau bisa juga untuk mempublikasikan feeds kustom. Pelajari lebih lanjut here
Cara Baca Data Dari Aggregator Feeds
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();
Membuat Aggregator Feed Baru
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());
Baca data dari aggregator feeds dalam sebuah program
Switchboard menyediakan sebuah crate yang dinamakan switchboard_v2 Pelajari lebih lanjut here
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()?;
Cara Membuat a Feeds Dari Publisher
Dokumentasi Switchboard resmi memiliki panduan mendalam tentang cara membuat feeds dari penerbit. Cek lebih lanjut here.
Oracles
Fitur unik Switchboard adalah memungkinkan Anda membuat oracle Anda sendiri dan menjalankannya secara lokal.
Create an 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,
});
Menjalan Oracle di lokal
Anda dapat menjalankan oracle secara lokal dan menetapkannya ke antrian oracle Anda sendiri untuk menguji bagaimana program Anda dapat beroperasi dalam environment production. Oracle Mainnet harus selalu dijalankan di lingkungan ketersediaan tinggi dengan beberapa kemampuan pemantauan.
Yang Dibutuhkan
- Docker-compose
Buat sebuah file docker-compose.yml dengan environment variable di Oracle Config
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
Jalankan container dengan docker-compose up
Konfigurasi Oracle
Env Variable | Definisi |
---|---|
ORACLE_KEY | Dibutuhkan Tipe - Public Key Deskripsi - Public key dari sebuah oracle account yang sudah mendapatkan izin untuk menggunakan oracle queue |
HEARTBEAT_INTERVAL | Opsional Tipe - Number (detik) Default - 30 Deskripsi - detik antara oracle heartbeats. Queues bisa memiliki oracle heartbeat yang berbeda. Nilai yang direkomendasikan adalah 15 |
GCP_CONFIG_BUCKET | Opsional Tipe - GCP Resource Path Default - Cari file configs.json di folder, jika tidak ditemukan maka tidak ada config yang di load. Deskripsi - Mengandung API keys untuk private API endpoints |
UNWRAP_STAKE_THRESHOLD | Optsonal Tipe - Number (jumlah SOL, Contoh. 1.55) Default - 0, disabled. Deskripsi - Jumlah saldo Solana untuk memicu tindakan pasak yang dibuka (_unwrap stake action_). Ketika balance Solana oracle turun di bawah ambang batas yang ditetapkan, node akan secara otomatis membuka dana dari dompet staking oracle, menyisakan setidaknya 0,1 wSOL atau 10% lebih banyak dari persyaratan stake minimum antrian. |
Fungsi Random yang Terverifikasi - Verifiable Random Function(VRF)
Verifiable Random Function (VRF) adalah fungsi pseudorandom dari public-key yang menyediakan bukti bahwa output telah di kalkukasi secara benar.
Membaca akun 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()?;
Membuat Sebuah Akun 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,
});
Request Randomness dari akun 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;
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,
)?;