Switchboard
Switchboard est un Oracle qui permet aux développeurs de s'approvisionner en données on-chain pour une variété de cas d'utilisation tels que l'obtention du prix de jetons, le prix plancher (floor price) de NFTs, les statistiques sportives ou même le caractère vérifiable du hasard. D'une manière générale, Switchboard est une ressource hors chaîne que les développeurs peuvent utiliser pour relier des données on-chain de haute qualité et alimenter la prochaine génération du Web3 et de la DeFi.
Flux de Données
Switchboard fournit une bibliothèque JavaScript/TypeScript appelée @switchboard-xyz/switchboard-v2. Cette bibliothèque peut être utilisée pour accéder aux données On-chain à partir de flux de données existants ou pour publier vos propres flux personnalisés. Plus d'informations à ce sujet ici
Lire les données d'un flux d'agrégation
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();
Créer un nouveau flux d'agrégation
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());
Lire les données d'un flux d'agrégation dans un programme
Switchboard fournit une crate appelée switchboard_v2. Plus d'informations à ce sujet ici
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()?;
Comment Créer un Flux à partir du Publisher
La documentation officielle de Switchboard explique en détail comment créer un flux à partir du Publisher. Découvrez-la ici.
Oracles
La caractéristique unique de Switchboard est qu'il vous permet de créer votre propre oracle et de l'exécuter localement.
Créer un 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,
});
Exécuter un oracle localement
Vous pouvez exécuter un oracle localement et l'affecter à votre propre file d'attente d'oracles pour tester comment votre programme peut fonctionner en production. Les oracles du Mainnet doivent toujours être exécutés dans des environnements à haute disponibilité avec un certain nombre de fonctionnalités de contrôle.
Conditions Requises
- Docker-compose
Créez un fichier docker-compose.yml avec les variables d'environnement dans 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
Exécutez le conteneur en utilisant docker-compose up
Configuration de l'Oracle
Variable Env | Définition |
---|---|
ORACLE_KEY | Obligatoire Type - Clé Publique Description - Clé publique du compte de l'oracle qui a reçu les permissions d'utiliser une file d'attente oracle |
HEARTBEAT_INTERVAL | Facultatif Type - Nombre (secondes) Par Défaut - 30 Description - Secondes entre les battements de cœur de l'oracle. Les files d'attente ont différentes exigences en matière de battement de cœur de l'oracle. La valeur recommandée est de 15 |
GCP_CONFIG_BUCKET | Facultatif Type - GCP Resource Path Par Défaut - Recherche le fichier configs.json dans le répertoire de travail actuel. Si elle n'est pas trouvée, aucune configuration n'est chargée. Description - Contient les clés API pour les points de terminaison API privés |
UNWRAP_STAKE_THRESHOLD | Facultatif Type - Nombre (montant de SOL amount, Ex. 1.55) Par Défaut - 0, désactivé. Description - Le montant de la balance Solana pour déclencher une action de déblocage de la mise. Lorsque le solde de Solana d'un oracle tombe sous le seuil fixé, le nœud débloque automatiquement les fonds du portefeuille de staking de l'oracle, laissant au moins 0,1 wSOL ou 10 % de plus que l'exigence de mise minimale de la file. |
Fonction Aléatoire Vérifiable(VRF)
Une Fonction Aléatoire Vérifiable (VRF) est une fonction pseudo-aléatoire à clé publique qui fournit des preuves que ses sorties ont été calculées correctement.
Lire un compte 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()?;
Créer un compte 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,
});
Demande de Hasard à partir d'un compte 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,
)?;
Ressources
API et Bibliothèques
- Types de Tâches de Switchboard
- Documents sur l'API Rust
- Documents sur l'API Typescript
- Documents sur l'API Python
- Documents sur le CLI
Exemples
- [Client] Visite Guidée du Flux de Données Personnalisé
- [Programme] Analyseur de Flux pour Anchor
- [Programme] Analyseur VRF pour Anchor