added buffer relayer impl

[RS] Added buffer relayer structs
[RS] Added check_staleness and check_confidence_interval to accounts
[CLI] Added buffer relayer commands
[Anchor] Added better data feed validation to examples
[TS] Added enableBufferRelayers to oracleQueueInit
This commit is contained in:
Conner Gallagher 2022-07-15 15:25:04 -06:00
parent 2f99623c3a
commit 32c9f86b24
62 changed files with 2540 additions and 334 deletions

View File

@ -2,6 +2,7 @@
members = [
"programs/anchor-feed-parser",
"programs/anchor-vrf-parser",
"programs/anchor-buffer-parser",
"programs/native-feed-parser"
]
@ -14,6 +15,7 @@ wallet = "../payer-keypair.json"
[programs.localnet]
anchor_feed_parser = "FnsPs665aBSwJRu2A8wGv6ZT76ipR41kHm4hoA3B1QGh"
anchor_vrf_parser = "HjjRFjCyQH3ne6Gg8Yn3TQafrrYecRrphwLwnh2A26vM"
anchor_buffer_parser = "96punQGZDShZGkzsBa3SsfTxfUnwu4XGpzXbhF7NTgcP"
[registry]
url = "https://anchor.projectserum.com"

View File

@ -1,4 +1,7 @@
[workspace]
members = [
"programs/*",
]
exclude = [
"libraries/rs"
]

View File

@ -3,7 +3,9 @@ import * as anchor from "@project-serum/anchor";
import { ACCOUNT_DISCRIMINATOR_SIZE } from "@project-serum/anchor/dist/cjs/coder";
import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js";
import {
DEFAULT_KEYPAIR,
prettyPrintAggregator,
prettyPrintBufferRelayer,
prettyPrintCrank,
prettyPrintJob,
prettyPrintLease,
@ -17,6 +19,7 @@ import {
} from "@switchboard-xyz/sbv2-utils";
import {
AggregatorAccount,
BufferRelayerAccount,
CrankAccount,
JobAccount,
LeaseAccount,
@ -159,7 +162,14 @@ abstract class PrintBaseCommand extends Command {
this.logger.log(await prettyPrintVrf(vrfAccount));
break;
}
case "BufferRelayerAccountData": {
const bufferRelayerAccount = new BufferRelayerAccount({
program,
publicKey,
});
this.logger.log(await prettyPrintBufferRelayer(bufferRelayerAccount));
break;
}
case "BUFFERxx": {
console.log(`Found buffer account but dont know which one`);
break;

View File

@ -0,0 +1,110 @@
import { Flags } from "@oclif/core";
import { PublicKey } from "@solana/web3.js";
import { prettyPrintBufferRelayer } from "@switchboard-xyz/sbv2-utils";
import {
BufferRelayerAccount,
JobAccount,
OracleJob,
OracleQueueAccount,
programWallet,
} from "@switchboard-xyz/switchboard-v2";
import fs from "fs";
import path from "path";
import BaseCommand from "../../BaseCommand";
import { verifyProgramHasPayer } from "../../utils";
export default class BufferCreate extends BaseCommand {
static description = "create a buffer relayer account";
static flags = {
...BaseCommand.flags,
authority: Flags.string({
char: "a",
description: "alternate keypair that will be the aggregator authority",
}),
name: Flags.string({
char: "n",
description: "name of the buffer account",
}),
minUpdateDelaySeconds: Flags.integer({
description: "minimum number of seconds between update calls",
default: 30,
}),
jobDefinition: Flags.string({
description: "filesystem path to job definition",
exclusive: ["jobKey"],
}),
jobKey: Flags.string({
description: "public key of existing job account",
exclusive: ["jobDefinition"],
}),
};
static args = [
{
name: "queueKey",
description: "oracle queue to create BufferRelayer account on",
},
];
async run() {
verifyProgramHasPayer(this.program);
const { args, flags } = await this.parse(BufferCreate);
const payerKeypair = programWallet(this.program);
const authority = await this.loadAuthority(flags.authority);
let jobAccount: JobAccount;
if (flags.jobDefinition) {
const jobDefinitionPath = flags.jobDefinition.startsWith("/")
? flags.jobDefinition
: path.join(process.cwd(), flags.jobDefinition);
if (!fs.existsSync(jobDefinitionPath)) {
throw new Error(
`jobDefinitionPath does not exist, ${jobDefinitionPath}`
);
}
const oracleJob = OracleJob.create(
JSON.parse(fs.readFileSync(jobDefinitionPath, "utf-8"))
);
jobAccount = await JobAccount.create(this.program, {
authority: authority.publicKey,
name: flags.name ? Buffer.from(flags.name) : Buffer.from(""),
data: Buffer.from(OracleJob.encodeDelimited(oracleJob).finish()),
});
} else if (flags.jobKey) {
jobAccount = new JobAccount({
program: this.program,
publicKey: new PublicKey(flags.jobKey),
});
} else {
throw new Error(`Need to provide --jobDefinition or --jobKey flag`);
}
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: new PublicKey(args.queueKey),
});
const bufferRelayerAccount = await BufferRelayerAccount.create(
this.program,
{
authority: authority.publicKey,
name: flags.name ? Buffer.from(flags.name) : Buffer.from(""),
minUpdateDelaySeconds: flags.minUpdateDelaySeconds,
queueAccount,
jobAccount,
}
);
if (this.silent) {
this.logger.info(bufferRelayerAccount.publicKey.toString());
return;
}
this.logger.info(await prettyPrintBufferRelayer(bufferRelayerAccount));
}
async catch(error) {
super.catch(error, "failed to create buffer relayer account");
}
}

View File

@ -27,6 +27,7 @@ export default class QueueAddCrank extends BaseCommand {
}),
maxRows: Flags.integer({
char: "r",
default: 100,
description: "maximum number of rows a crank can support",
}),
queueAuthority: Flags.string({

View File

@ -11,13 +11,13 @@ import BaseCommand from "../../../BaseCommand";
import { verifyProgramHasPayer } from "../../../utils";
export default class JobCreate extends BaseCommand {
static description = "create a buffer relayer account";
static description = "create a job account";
static flags = {
...BaseCommand.flags,
authority: Flags.string({
char: "a",
description: "alternate keypair that will be the aggregator authority",
description: "alternate keypair that will be the account authority",
}),
name: Flags.string({
char: "n",
@ -49,22 +49,10 @@ export default class JobCreate extends BaseCommand {
const oracleJob = OracleJob.create(
JSON.parse(fs.readFileSync(jobDefinitionPath, "utf-8"))
);
if (!("tasks" in oracleJob)) {
throw new Error("Must provide tasks in job definition");
}
const data = Buffer.from(
OracleJob.encodeDelimited(
OracleJob.create({
tasks: oracleJob.tasks,
})
).finish()
);
console.log(`DATA: [${data.join(",")}]`);
const jobAccount = await JobAccount.create(this.program, {
authority: authority.publicKey,
name: flags.name ? Buffer.from(flags.name) : Buffer.from(""),
data,
data: Buffer.from(OracleJob.encodeDelimited(oracleJob).finish()),
});
if (this.silent) {
@ -72,13 +60,11 @@ export default class JobCreate extends BaseCommand {
return;
}
const job = await jobAccount.loadData();
this.logger.info(await prettyPrintJob(jobAccount, job));
console.log(`DATA: [${job.data.join(",")}]`);
const jobData = await jobAccount.loadData();
this.logger.info(await prettyPrintJob(jobAccount, jobData));
}
async catch(error) {
super.catch(error, "failed to create buffer relayer account");
super.catch(error, "failed to create job account");
}
}

View File

@ -0,0 +1,53 @@
/* eslint-disable unicorn/import-style */
import { Flags } from "@oclif/core";
import { prettyPrintBufferRelayer } from "@switchboard-xyz/sbv2-utils";
import { BufferRelayerAccount } from "@switchboard-xyz/switchboard-v2";
import BaseCommand from "../../BaseCommand";
export default class BufferPrint extends BaseCommand {
static description =
"Print the deserialized Switchboard buffer relayer account";
static aliases = ["buffer:print"];
static flags = {
...BaseCommand.flags,
job: Flags.boolean({
description: "output job definitions",
default: false,
}),
};
static args = [
{
name: "bufferRelayerKey",
description: "public key of the buffer relayer account to deserialize",
},
];
static examples = [
"$ sbv2 buffer:print 23GvzENjwgqqaLejsAtAWgTkSzWjSMo2LUYTAETT8URp",
];
async run() {
const { args, flags } = await this.parse(BufferPrint);
const bufferAccount = new BufferRelayerAccount({
program: this.program,
publicKey: args.bufferRelayerKey,
});
const bufferAccountData = await bufferAccount.loadData();
this.logger.log(
await prettyPrintBufferRelayer(
bufferAccount,
bufferAccountData,
flags.job
)
);
}
async catch(error) {
super.catch(error, "failed to print buffer relayer account");
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "switchboard-v2"
version = "0.1.10"
version = "0.1.11"
edition = "2021"
description = "A Rust library to interact with Switchboard V2 data feeds."
readme = "README.md"
@ -13,6 +13,7 @@ documentation = "https://docs.rs/switchboard-v2/"
[lib]
crate-type = ["cdylib", "lib"]
name = "switchboard_v2"
doctest = false
[features]
default = ["cpi"]

View File

@ -19,12 +19,27 @@ from a provided data feed AccountInfo.
### Aggregator
```rust
use switchboard_v2::AggregatorAccountData;
use anchor_lang::solana_program::clock;
use std::convert::TryInto;
use switchboard_v2::{AggregatorAccountData, SWITCHBOARD_V2_DEVNET, SWITCHBOARD_V2_MAINNET};
let feed_result = AggregatorAccountData::new(feed_account_info)?.get_result()?;
// check feed owner
let owner = *aggregator.owner;
if owner != SWITCHBOARD_V2_DEVNET && owner != SWITCHBOARD_V2_MAINNET {
return Err(error!(ErrorCode::InvalidSwitchboardAccount));
}
let decimal: f64 = feed_result.try_into()?;
// deserialize account info
let feed = AggregatorAccountData::new(feed_account_info)?;
// get result
let decimal: f64 = feed.get_result()?.try_into()?;
// check if feed has been updated in the last 5 minutes
feed.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)?;
// check if feed exceeds a confidence interval of +/i $0.80
feed.check_confidence_interval(0.80)?;
```
### Aggregator History
@ -45,11 +60,34 @@ 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;
```
### Buffer Relayer Account
```rust
use anchor_lang::solana_program::clock;
use std::convert::TryInto;
use switchboard_v2::{BufferRelayerAccountData, SWITCHBOARD_V2_DEVNET, SWITCHBOARD_V2_MAINNET};
// check feed owner
let owner = *aggregator.owner;
if owner != SWITCHBOARD_V2_DEVNET && owner != SWITCHBOARD_V2_MAINNET {
return Err(error!(ErrorCode::InvalidSwitchboardAccount));
}
// deserialize account info
let buffer = BufferRelayerAccountData::new(feed_account_info)?;
// get result
let buffer_result = buffer.get_result();
// check if feed has been updated in the last 5 minutes
buffer.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)?;
// convert buffer to a string
let result_string = String::from_utf8(buffer.result)
.map_err(|_| error!(ErrorCode::StringConversionFailed))?;
msg!("Buffer string {:?}!", result_string);
```

View File

@ -34,10 +34,10 @@ pub struct AggregatorRound {
pub min_response: SwitchboardDecimal,
// Maintains the maximum node response this round.
pub max_response: SwitchboardDecimal,
// pub lease_key: Option<Pubkey>,
// pub lease_key: Pubkey,
// Pubkeys of the oracles fulfilling this round.
pub oracle_pubkeys_data: [Pubkey; 16],
// pub oracle_pubkeys_size: Option<u32>, IMPLIED BY ORACLE_REQUEST_BATCH_SIZE
// pub oracle_pubkeys_size: u32, IMPLIED BY ORACLE_REQUEST_BATCH_SIZE
// Represents all successful node responses this round. `NaN` if empty.
pub medians_data: [SwitchboardDecimal; 16],
// Current rewards/slashes oracles have received this round.
@ -55,39 +55,54 @@ pub struct AggregatorRound {
pub struct AggregatorAccountData {
pub name: [u8; 32],
pub metadata: [u8; 128],
pub author_wallet: Pubkey,
pub _reserved1: [u8; 32],
pub queue_pubkey: Pubkey,
// CONFIGS
// affects update price, shouldnt be changeable
pub oracle_request_batch_size: u32,
pub min_oracle_results: u32,
pub min_job_results: u32,
// affects update price, shouldnt be changeable
pub min_update_delay_seconds: u32,
// timestamp to start feed updates at
pub start_after: i64,
pub start_after: i64, // timestamp to start feed updates at
pub variance_threshold: SwitchboardDecimal,
// If no feed results after this period, trigger nodes to report
pub force_report_period: i64,
pub force_report_period: i64, // If no feed results after this period, trigger nodes to report
pub expiration: i64,
//
pub consecutive_failure_count: u64,
pub next_allowed_update_time: i64,
pub is_locked: bool,
pub _schedule: [u8; 32],
pub crank_pubkey: Pubkey,
pub latest_confirmed_round: AggregatorRound,
pub current_round: AggregatorRound,
pub job_pubkeys_data: [Pubkey; 16],
pub job_hashes: [Hash; 16],
pub job_pubkeys_size: u32,
// Used to confirm with oracles they are answering what they think theyre answering
pub jobs_checksum: [u8; 32],
pub jobs_checksum: [u8; 32], // Used to confirm with oracles they are answering what they think theyre answering
//
pub authority: Pubkey,
pub _ebuf: [u8; 224], // Buffer for future info
pub history_buffer: Pubkey,
pub previous_confirmed_round_result: SwitchboardDecimal,
pub previous_confirmed_round_slot: u64,
pub disable_crank: bool,
pub job_weights: [u8; 16],
pub _ebuf: [u8; 147], // Buffer for future info
}
unsafe impl Pod for AggregatorAccountData {}
unsafe impl Zeroable for AggregatorAccountData {}
impl AggregatorAccountData {
/// Returns the deserialized Switchboard Aggregator account
///
/// # Arguments
///
/// * `switchboard_feed` - A Solana AccountInfo referencing an existing Switchboard Aggregator
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::AggregatorAccountData;
///
/// let data_feed = AggregatorAccountData::new(feed_account_info)?;
/// ```
pub fn new<'info>(
switchboard_feed: &'info AccountInfo,
) -> anchor_lang::Result<Ref<'info, AggregatorAccountData>> {
@ -96,13 +111,22 @@ impl AggregatorAccountData {
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
if disc_bytes != AggregatorAccountData::discriminator() {
msg!("{:?}", disc_bytes);
return Err(SwitchboardError::AccountDiscriminatorMismatch.into());
}
Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..])))
}
/// If sufficient oracle responses, returns the latest on-chain result in SwitchboardDecimal format
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::AggregatorAccountData;
/// use std::convert::TryInto;
///
/// let feed_result = AggregatorAccountData::new(feed_account_info)?.get_result()?;
/// let decimal: f64 = feed_result.try_into()?;
/// ```
pub fn get_result(&self) -> anchor_lang::Result<SwitchboardDecimal> {
if self.min_oracle_results > self.latest_confirmed_round.num_success {
return Err(SwitchboardError::InvalidAggregatorRound.into());
@ -110,12 +134,53 @@ impl AggregatorAccountData {
Ok(self.latest_confirmed_round.result)
}
/// Check whether the confidence interval exceeds a given threshold
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::{AggregatorAccountData, SwitchboardDecimal};
///
/// let feed = AggregatorAccountData::new(feed_account_info)?;
/// feed.check_confidence_interval(SwitchboardDecimal::from_f64(0.80))?;
/// ```
pub fn check_confidence_interval(
&self,
max_confidence_interval: SwitchboardDecimal,
) -> anchor_lang::Result<()> {
if self.latest_confirmed_round.std_deviation > max_confidence_interval {
return Err(SwitchboardError::ConfidenceIntervalExceeded.into());
}
Ok(())
}
/// Check whether the feed has been updated in the last max_staleness seconds
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::AggregatorAccountData;
///
/// let feed = AggregatorAccountData::new(feed_account_info)?;
/// feed.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)?;
/// ```
pub fn check_staleness(
&self,
unix_timestamp: i64,
max_staleness: i64,
) -> anchor_lang::Result<()> {
let staleness = unix_timestamp - self.latest_confirmed_round.round_open_timestamp;
if staleness > max_staleness {
msg!("Feed has not been updated in {} seconds!", staleness);
return Err(SwitchboardError::StaleFeed.into());
}
Ok(())
}
fn discriminator() -> [u8; 8] {
return [217, 230, 65, 101, 201, 162, 27, 125];
}
}
unsafe impl Pod for AggregatorAccountData {}
unsafe impl Zeroable for AggregatorAccountData {}
#[cfg(test)]
mod tests {

View File

@ -0,0 +1,88 @@
use super::error::SwitchboardError;
use anchor_lang::prelude::*;
use solana_program::pubkey::Pubkey;
#[derive(AnchorDeserialize, Default, Debug)]
pub struct BufferRelayerAccountData {
pub name: [u8; 32],
pub queue_pubkey: Pubkey,
pub escrow: Pubkey,
pub authority: Pubkey,
pub job_pubkey: Pubkey,
pub job_hash: [u8; 32],
pub min_update_delay_seconds: u32,
pub is_locked: bool,
pub current_round: BufferRelayerRound,
pub latest_confirmed_round: BufferRelayerRound,
pub result: Vec<u8>,
}
#[derive(Default, Debug, Clone, AnchorSerialize, AnchorDeserialize)]
pub struct BufferRelayerRound {
pub num_success: u32,
pub num_error: u32,
pub round_open_slot: u64,
pub round_open_timestamp: i64,
pub oracle_pubkey: Pubkey,
}
impl BufferRelayerAccountData {
/// Returns the deserialized Switchboard Buffer Relayer account
///
/// # Arguments
///
/// * `switchboard_buffer` - A Solana AccountInfo referencing an existing Switchboard BufferRelayer
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::BufferRelayerAccountData;
///
/// let buffer_account = BufferRelayerAccountData::new(buffer_account_info)?;
/// ```
pub fn new<'a>(
switchboard_buffer: &'a AccountInfo,
) -> anchor_lang::Result<Box<BufferRelayerAccountData>> {
let data = switchboard_buffer.try_borrow_data()?;
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
if disc_bytes != BufferRelayerAccountData::discriminator() {
return Err(SwitchboardError::AccountDiscriminatorMismatch.into());
}
let mut v_mut = &data[8..];
Ok(Box::new(BufferRelayerAccountData::deserialize(&mut v_mut)?))
}
pub fn get_result(&self) -> &Vec<u8> {
return &self.result;
}
/// Check whether the buffer relayer has been updated in the last max_staleness seconds
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::BufferRelayerAccountData;
///
/// let buffer = BufferRelayerAccountData::new(buffer_account_info)?;
/// buffer.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)?;
/// ```
pub fn check_staleness(
&self,
unix_timestamp: i64,
max_staleness: i64,
) -> anchor_lang::Result<()> {
let staleness = unix_timestamp - self.latest_confirmed_round.round_open_timestamp;
if staleness > max_staleness {
msg!("Feed has not been updated in {} seconds!", staleness);
return Err(SwitchboardError::StaleFeed.into());
}
Ok(())
}
fn discriminator() -> [u8; 8] {
return [50, 35, 51, 115, 169, 219, 158, 52];
}
}

View File

@ -3,20 +3,26 @@ use anchor_lang::prelude::*;
#[error_code]
#[derive(Eq, PartialEq)]
pub enum SwitchboardError {
#[msg("Aggregator is not currently populated with a valid round.")]
#[msg("Aggregator is not currently populated with a valid round")]
InvalidAggregatorRound,
#[msg("Failed to convert string to decimal format.")]
#[msg("Failed to convert string to decimal format")]
InvalidStrDecimalConversion,
#[msg("Decimal conversion method failed.")]
#[msg("Decimal conversion method failed")]
DecimalConversionError,
#[msg("An integer overflow occurred.")]
#[msg("An integer overflow occurred")]
IntegerOverflowError,
#[msg("Account discriminator did not match.")]
#[msg("Account discriminator did not match")]
AccountDiscriminatorMismatch,
#[msg("Vrf value is empty.")]
#[msg("Vrf value is empty")]
VrfEmptyError,
#[msg("Failed to send requestRandomness instruction")]
VrfCpiError,
#[msg("Failed to send signed requestRandomness instruction")]
VrfCpiSignedError,
#[msg("Failed to deserialize account")]
AccountDeserializationError,
#[msg("Switchboard feed exceeded the staleness threshold")]
StaleFeed,
#[msg("Switchboard feed exceeded the confidence interval threshold")]
ConfidenceIntervalExceeded,
}

View File

@ -23,6 +23,11 @@ pub struct AggregatorHistoryBuffer<'a> {
pub rows: Ref<'a, [AggregatorHistoryRow]>,
}
impl<'a> AggregatorHistoryBuffer<'a> {
/// Returns the deserialized Switchboard history buffer account
///
/// # Arguments
///
/// * `history_buffer` - A Solana AccountInfo referencing an existing Switchboard history buffer account
pub fn new(
history_buffer: &'a AccountInfo,
) -> anchor_lang::Result<AggregatorHistoryBuffer<'a>> {
@ -41,7 +46,11 @@ impl<'a> AggregatorHistoryBuffer<'a> {
});
}
/// return the previous row in the history buffer for a given timestamp
/// Return the previous row in the history buffer for a given timestamp
///
/// # Arguments
///
/// * `timestamp` - A unix timestamp to search in the history buffer
pub fn lower_bound(&self, timestamp: i64) -> Option<AggregatorHistoryRow> {
if self.rows[self.insertion_idx].timestamp == 0 {
return None;

View File

@ -1,15 +1,16 @@
use anchor_spl::token::TokenAccount;
pub mod aggregator;
pub mod buffer_relayer;
pub mod decimal;
pub mod error;
pub mod history_buffer;
pub mod vrf;
pub use aggregator::AggregatorAccountData;
pub use history_buffer::AggregatorHistoryBuffer;
pub use vrf::VrfAccountData;
pub use vrf::VrfRequestRandomness;
pub use aggregator::{AggregatorAccountData, AggregatorRound};
pub use buffer_relayer::{BufferRelayerAccountData, BufferRelayerRound};
pub use decimal::SwitchboardDecimal;
pub use error::SwitchboardError;
pub use history_buffer::{AggregatorHistoryBuffer, AggregatorHistoryRow};
pub use vrf::{VrfAccountData, VrfRequestRandomness, VrfRound, VrfStatus};
/// Mainnet program id for Switchboard v2. Prints out as "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
pub const SWITCHBOARD_V2_MAINNET: solana_program::pubkey::Pubkey =

View File

@ -1,8 +1,8 @@
#![allow(non_snake_case)]
use super::error::SwitchboardError;
#[allow(unaligned_references)]
use crate::*;
use super::error::SwitchboardError;
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use bytemuck::{Pod, Zeroable};
use solana_program::account_info::AccountInfo;
use solana_program::instruction::Instruction;
@ -316,6 +316,8 @@ pub struct Callback {
#[zero_copy]
#[repr(packed)]
pub struct VrfRound {
// pub producer_pubkeys: [Pubkey; 16],
// pub producer_pubkeys_len: u32,
pub alpha: [u8; 256],
pub alpha_len: u32,
pub request_slot: u64,
@ -356,7 +358,6 @@ pub struct VrfAccountData {
pub test_mode: bool,
// pub last_verified_round: VrfRound,
pub current_round: VrfRound,
//
pub _ebuf: [u8; 1024], // Buffer for future info
}
impl Default for VrfAccountData {
@ -366,6 +367,19 @@ impl Default for VrfAccountData {
}
impl VrfAccountData {
/// Returns the deserialized Switchboard VRF account
///
/// # Arguments
///
/// * `switchboard_vrf` - A Solana AccountInfo referencing an existing Switchboard VRF account
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::VrfAccountData;
///
/// let vrf = VrfAccountData::new(vrf_account_info)?;
/// ```
pub fn new<'info>(
switchboard_vrf: &'info AccountInfo,
) -> anchor_lang::Result<Ref<'info, VrfAccountData>> {
@ -374,17 +388,27 @@ impl VrfAccountData {
let mut disc_bytes = [0u8; 8];
disc_bytes.copy_from_slice(&data[..8]);
if disc_bytes != VrfAccountData::discriminator() {
msg!("{:?}", disc_bytes);
return Err(error!(SwitchboardError::AccountDiscriminatorMismatch));
}
Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..])))
}
/// Returns the current VRF round ID
pub fn get_current_randomness_round_id(&self) -> u128 {
self.counter
}
/// If sufficient oracle responses, returns the latest on-chain result in SwitchboardDecimal format
///
/// # Examples
///
/// ```ignore
/// use switchboard_v2::AggregatorAccountData;
/// use std::convert::TryInto;
///
/// let feed_result = AggregatorAccountData::new(feed_account_info)?.get_result()?;
/// let decimal: f64 = feed_result.try_into()?;
/// ```
pub fn get_result(&self) -> anchor_lang::Result<[u8; 32]> {
if self.current_round.result == [0u8; 32] {
return Err(error!(SwitchboardError::VrfEmptyError));
@ -1628,4 +1652,4 @@ mod tests {
assert_eq!(vrf.get_result().unwrap(), VRF_RESULT);
assert_eq!(vrf.status, VrfStatus::StatusCallbackSuccess);
}
}
}

View File

@ -37,7 +37,7 @@
"@project-serum/anchor": "^0.24.2",
"@saberhq/token-utils": "^1.13.32",
"@solana/spl-token": "^0.2.0",
"@solana/web3.js": "^1.42.0",
"@solana/web3.js": "^1.47.3",
"@switchboard-xyz/switchboard-v2": "^0.0.121",
"big.js": "^6.1.1",
"bn.js": "^5.2.1",

View File

@ -2,6 +2,7 @@ import * as anchor from "@project-serum/anchor";
import { AccountMeta, PublicKey, TokenAmount } from "@solana/web3.js";
import {
AggregatorAccount,
BufferRelayerAccount,
CrankAccount,
CrankRow,
JobAccount,
@ -15,7 +16,7 @@ import {
SwitchboardPermissionValue,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
import type Big from "big.js";
import Big from "big.js";
import chalk from "chalk";
import { getIdlAddress, getProgramDataAddress } from "./anchor.js";
import { anchorBNtoDateTimeString } from "./date.js";
@ -47,6 +48,28 @@ export const chalkString = (
)}`;
};
// JSON.stringify: Object => String
export const pubKeyConverter = (key: any, value: any): any => {
if (value instanceof PublicKey || key.toLowerCase().endsWith("publickey")) {
return value.toString() ?? "";
}
if (value instanceof Uint8Array) {
return `[${value.toString()}]`;
}
if (value instanceof anchor.BN) {
return value.toString();
}
if (value instanceof Big) {
return value.toString();
}
if (value instanceof SwitchboardDecimal) {
return new Big(value.mantissa.toString())
.div(new Big(10).pow(value.scale))
.toString();
}
return value;
};
export const tokenAmountString = (value: TokenAmount): string => {
return `${value.uiAmountString ?? ""} (${value.amount})`;
};
@ -379,8 +402,7 @@ export async function prettyPrintJob(
outputString +=
chalkString("metadata", buffer2string(data.metadata as any), SPACING) +
"\r\n";
outputString +=
chalkString("authorWallet", data.authorWallet, SPACING) + "\r\n";
outputString += chalkString("authority", data.authority, SPACING) + "\r\n";
outputString += chalkString("expiration", data.expiration, SPACING) + "\r\n";
outputString += chalkString(
"tasks",
@ -416,7 +438,11 @@ export async function prettyPrintAggregator(
let outputString = "";
outputString += chalk.underline(
chalkString("## Aggregator", aggregatorAccount.publicKey!, SPACING) + "\r\n"
chalkString(
"## Aggregator",
aggregatorAccount.publicKey ?? PublicKey.default,
SPACING
) + "\r\n"
);
outputString +=
@ -477,7 +503,7 @@ export async function prettyPrintAggregator(
aggregatorAccount.program,
queue.authority,
queueAccount.publicKey,
aggregatorAccount.publicKey!
aggregatorAccount.publicKey ?? PublicKey.default
);
const permissionData = await permissionAccount.loadData();
outputString +=
@ -702,6 +728,56 @@ export async function prettyPrintCrank(
return outputString;
}
export async function prettyPrintBufferRelayer(
bufferRelayerAccount: BufferRelayerAccount,
accountData?: any,
printJob = false,
SPACING = 24
): Promise<string> {
const data = accountData ?? (await bufferRelayerAccount.loadData());
let outputString = "";
outputString += chalk.underline(
chalkString("## BufferRelayer", bufferRelayerAccount.publicKey, SPACING) +
"\r\n"
);
outputString +=
chalkString("name", buffer2string(data.name as any), SPACING) + "\r\n";
outputString +=
chalkString("queuePubkey", data.queuePubkey, SPACING) + "\r\n";
outputString += chalkString("escrow", data.escrow, SPACING) + "\r\n";
outputString += chalkString("authority", data.authority, SPACING) + "\r\n";
outputString += chalkString("jobPubkey", data.jobPubkey, SPACING) + "\r\n";
outputString +=
chalkString("minUpdateDelaySeconds", data.minUpdateDelaySeconds, SPACING) +
"\r\n";
const result = data.result as number[];
outputString +=
chalkString(
"result",
`[${result.map((r) => r.toString()).join(",")}]`,
SPACING
) + "\r\n";
outputString +=
chalkString(
"currentRound",
JSON.stringify(data.currentRound, pubKeyConverter, 2),
SPACING
) + "\r\n";
if (printJob) {
const jobAccount = new JobAccount({
program: bufferRelayerAccount.program,
publicKey: data.jobPubkey,
});
outputString += "\r\n" + (await prettyPrintJob(jobAccount));
}
return outputString;
}
export async function prettyPrintSwitchboardAccount(
program: anchor.Program,
publicKey: PublicKey,
@ -745,6 +821,13 @@ export async function prettyPrintSwitchboardAccount(
const vrfAccount = new VrfAccount({ program, publicKey });
return prettyPrintVrf(vrfAccount, undefined);
}
case "BufferRelayerAccountData": {
const bufferRelayerAccount = new BufferRelayerAccount({
program,
publicKey,
});
return prettyPrintBufferRelayer(bufferRelayerAccount, undefined);
}
case "BUFFERxx": {
return `Found buffer account but dont know which one`;
}

View File

@ -30,6 +30,7 @@ export interface CreateQueueParams {
numOracles?: number;
unpermissionedFeeds?: boolean;
unpermissionedVrf?: boolean;
enableBufferRelayers?: boolean;
}
export interface CreateQueueResponse {
@ -152,6 +153,7 @@ export async function createQueue(
queueSize: queueSize,
unpermissionedFeeds: params.unpermissionedFeeds ?? false,
unpermissionedVrf: params.unpermissionedVrf ?? false,
enableBufferRelayers: params.enableBufferRelayers ?? false,
})
.accounts({
oracleQueue: queueKeypair.publicKey,
@ -282,16 +284,16 @@ export async function createQueue(
})
);
const createAccountSignatures = packAndSend(
const createAccountSignatures = await packAndSend(
program,
[ixns, finalTransactions],
signers,
payerKeypair.publicKey
);
const result = await program.provider.connection.confirmTransaction(
createAccountSignatures[-1]
);
// const result = await program.provider.connection.confirmTransaction(
// createAccountSignatures[-1]
// );
return {
queueAccount,

View File

@ -3,6 +3,7 @@ import { ACCOUNT_DISCRIMINATOR_SIZE } from "@project-serum/anchor";
import type { PublicKey } from "@solana/web3.js";
import {
AggregatorAccount,
BufferRelayerAccount,
CrankAccount,
JobAccount,
LeaseAccount,
@ -26,6 +27,7 @@ export const SWITCHBOARD_ACCOUNT_TYPES = [
"SbState",
"BUFFERxx",
"CrankAccountData",
"BufferRelayerAccountData",
] as const;
export type SwitchboardAccount =
@ -37,7 +39,8 @@ export type SwitchboardAccount =
| LeaseAccount
| ProgramStateAccount
| VrfAccount
| CrankAccount;
| CrankAccount
| BufferRelayerAccount;
export type SwitchboardAccountType = typeof SWITCHBOARD_ACCOUNT_TYPES[number];
@ -109,6 +112,9 @@ export const loadSwitchboardAccount = async (
case "VrfAccountData": {
return [accountType, new VrfAccount({ program, publicKey })];
}
case "BufferRelayerAccountData": {
return [accountType, new BufferRelayerAccount({ program, publicKey })];
}
}
throw new InvalidSwitchboardAccount();

View File

@ -1,11 +1,19 @@
import * as anchor from "@project-serum/anchor";
import * as spl from "@solana/spl-token";
import { clusterApiUrl, Connection, Keypair, PublicKey } from "@solana/web3.js";
import * as sbv2 from "@switchboard-xyz/switchboard-v2";
import {
CrankAccount,
OracleAccount,
PermissionAccount,
ProgramStateAccount,
} from "@switchboard-xyz/switchboard-v2";
import chalk from "chalk";
import fs from "fs";
import path from "path";
import { getIdlAddress, getProgramDataAddress } from "../anchor.js";
import { anchorBNtoDateString } from "../date.js";
import { createQueue } from "../queue.js";
import { getOrCreateSwitchboardTokenAccount } from "../state.js";
const LATEST_DOCKER_VERSION = "dev-v2-07-11-22";
@ -315,95 +323,150 @@ secrets:
);
const idlAddress = await getIdlAddress(switchboardProgram.programId);
const [switchboardProgramState] =
sbv2.ProgramStateAccount.fromSeed(switchboardProgram);
let programState: any;
try {
programState = await switchboardProgramState.loadData();
} catch {
await sbv2.ProgramStateAccount.create(switchboardProgram, {
mint: spl.NATIVE_MINT,
daoMint: spl.NATIVE_MINT,
});
programState = await switchboardProgramState.loadData();
}
// const [switchboardProgramState] =
// sbv2.ProgramStateAccount.fromSeed(switchboardProgram);
// let programState: any;
// try {
// programState = await switchboardProgramState.loadData();
// } catch {
// await sbv2.ProgramStateAccount.create(switchboardProgram, {
// mint: spl.NATIVE_MINT,
// daoMint: spl.NATIVE_MINT,
// });
// programState = await switchboardProgramState.loadData();
// }
const mint = await switchboardProgramState.getTokenMint();
// const mint = await switchboardProgramState.getTokenMint();
const payerSwitchboardWallet = (
await spl.getOrCreateAssociatedTokenAccount(
connection,
payerKeypair,
mint.address,
payerKeypair.publicKey,
undefined,
undefined,
undefined,
spl.TOKEN_PROGRAM_ID,
spl.ASSOCIATED_TOKEN_PROGRAM_ID
)
).address;
// const payerSwitchboardWallet = (
// await spl.getOrCreateAssociatedTokenAccount(
// connection,
// payerKeypair,
// mint.address,
// payerKeypair.publicKey,
// undefined,
// undefined,
// undefined,
// spl.TOKEN_PROGRAM_ID,
// spl.ASSOCIATED_TOKEN_PROGRAM_ID
// )
// ).address;
// create queue with unpermissioned VRF accounts enabled
const queueAccount = await sbv2.OracleQueueAccount.create(
// // create queue with unpermissioned VRF accounts enabled
// const queueAccount = await sbv2.OracleQueueAccount.create(
// switchboardProgram,
// {
// name: Buffer.from("My Test Queue"),
// mint: spl.NATIVE_MINT,
// authority: payerKeypair.publicKey, // Approve new participants
// minStake: new anchor.BN(0), // Oracle minStake to heartbeat
// reward: new anchor.BN(0), // Oracle rewards per request (non-VRF)
// queueSize: 10, // Number of active oracles a queue can support
// unpermissionedFeeds: true, // Whether feeds need PERMIT_ORACLE_QUEUE_USAGE permissions
// unpermissionedVrf: true, // Whether VRF accounts need PERMIT_VRF_REQUESTS permissions
// enableBufferRelayers: true,
// }
// );
// await queueAccount.setVrfSettings({
// authority: payerKeypair,
// unpermissionedVrf: true,
// });
// const queue = await queueAccount.loadData();
// // create a crank for the queue
// const crankAccount = await sbv2.CrankAccount.create(switchboardProgram, {
// name: Buffer.from("My Crank"),
// maxRows: 100,
// queueAccount,
// });
// const crank = await crankAccount.loadData();
// // create oracle to run locally
// const oracleAccount = await sbv2.OracleAccount.create(switchboardProgram, {
// name: Buffer.from("My Oracle"),
// oracleAuthority: payerKeypair,
// queueAccount,
// });
// const oracle = await oracleAccount.loadData();
// // grant oracle heartbeat permissions
// const oraclePermissionAccount = await sbv2.PermissionAccount.create(
// switchboardProgram,
// {
// authority: queue.authority,
// granter: queueAccount.publicKey,
// grantee: oracleAccount.publicKey,
// }
// );
// await oraclePermissionAccount.set({
// authority: payerKeypair,
// enable: true,
// permission: sbv2.SwitchboardPermission.PERMIT_ORACLE_HEARTBEAT,
// });
const queueResponse = await createQueue(
switchboardProgram,
{
name: Buffer.from("My Test Queue"),
mint: spl.NATIVE_MINT,
authority: payerKeypair.publicKey, // Approve new participants
minStake: new anchor.BN(0), // Oracle minStake to heartbeat
reward: new anchor.BN(0), // Oracle rewards per request (non-VRF)
queueSize: 10, // Number of active oracles a queue can support
unpermissionedFeeds: true, // Whether feeds need PERMIT_ORACLE_QUEUE_USAGE permissions
unpermissionedVrf: true, // Whether VRF accounts need PERMIT_VRF_REQUESTS permissions
}
authority: payerKeypair.publicKey,
name: "Test Queue",
metadata: `created ${anchorBNtoDateString(
new anchor.BN(Math.floor(Date.now() / 1000))
)}`,
minStake: new anchor.BN(0),
reward: new anchor.BN(0),
crankSize: 10,
oracleTimeout: 180,
numOracles: 1,
unpermissionedFeeds: true,
unpermissionedVrf: true,
enableBufferRelayers: true,
},
10
);
await queueAccount.setVrfSettings({
authority: payerKeypair,
unpermissionedVrf: true,
});
const queueAccount = queueResponse.queueAccount;
const queue = await queueAccount.loadData();
// create a crank for the queue
const crankAccount = await sbv2.CrankAccount.create(switchboardProgram, {
name: Buffer.from("My Crank"),
maxRows: 100,
queueAccount,
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(switchboardProgram);
const programState = await programStateAccount.loadData();
const mint = await queueAccount.loadMint();
const payerSwitchboardWallet = await getOrCreateSwitchboardTokenAccount(
switchboardProgram,
mint
);
const crankAccount = new CrankAccount({
program: switchboardProgram,
publicKey: queueResponse.crankPubkey,
});
const crank = await crankAccount.loadData();
// create oracle to run locally
const oracleAccount = await sbv2.OracleAccount.create(switchboardProgram, {
name: Buffer.from("My Oracle"),
oracleAuthority: payerKeypair,
queueAccount,
const oracleAccount = new OracleAccount({
program: switchboardProgram,
publicKey: queueResponse.oracles[0],
});
const oracle = await oracleAccount.loadData();
// grant oracle heartbeat permissions
const oraclePermissionAccount = await sbv2.PermissionAccount.create(
const [permissionAccount] = PermissionAccount.fromSeed(
switchboardProgram,
{
authority: queue.authority,
granter: queueAccount.publicKey,
grantee: oracleAccount.publicKey,
}
queue.authority,
queueAccount.publicKey,
oracleAccount.publicKey
);
await oraclePermissionAccount.set({
authority: payerKeypair,
enable: true,
permission: sbv2.SwitchboardPermission.PERMIT_ORACLE_HEARTBEAT,
});
const permission = await permissionAccount.loadData();
const ctx: ISwitchboardTestEnvironment = {
programId: switchboardProgram.programId,
programDataAddress,
idlAddress,
programState: switchboardProgramState.publicKey,
programState: programStateAccount.publicKey,
switchboardVault: programState.tokenVault,
switchboardMint: mint.address,
tokenWallet: payerSwitchboardWallet,
queue: queueAccount.publicKey,
queue: queueResponse.queueAccount.publicKey,
queueAuthority: queue.authority,
queueBuffer: queue.dataBuffer,
crank: crankAccount.publicKey,
@ -411,7 +474,7 @@ secrets:
oracle: oracleAccount.publicKey,
oracleAuthority: oracle.oracleAuthority,
oracleEscrow: oracle.tokenAccount,
oraclePermissions: oraclePermissionAccount.publicKey,
oraclePermissions: permissionAccount.publicKey,
payerKeypairPath: fullKeypairPath,
additionalClonedAccounts,
};

View File

@ -37,14 +37,10 @@ export async function packAndSend(
sendAndConfirmRawTransaction(program.provider.connection, rawTx, {
maxRetries: 10,
commitment: "processed",
}).catch((error) => {
console.error(error);
throw error;
})
.then((sig) => {
return sig;
})
.catch((error) => {
console.error(error);
throw error;
})
);
}

View File

@ -2092,15 +2092,19 @@ export interface OracleQueueInitParams {
*/
queueSize?: number;
/**
* Eanbling this setting means data feeds do not need explicit permission
* Enabling this setting means data feeds do not need explicit permission
* to join the queue.
*/
unpermissionedFeeds?: boolean;
/**
* Eanbling this setting means data feeds do not need explicit permission
* Enabling this setting means data feeds do not need explicit permission
* to request VRF proofs and verifications from this queue.
*/
unpermissionedVrf?: boolean;
/**
* Enabling this setting will allow buffer relayer accounts to call openRound.
*/
enableBufferRelayers?: boolean;
mint: PublicKey;
}
@ -2236,6 +2240,7 @@ export class OracleQueueAccount {
minimumDelaySeconds: params.minimumDelaySeconds ?? 5,
queueSize: params.queueSize,
unpermissionedFeeds: params.unpermissionedFeeds ?? false,
enableBufferRelayers: params.enableBufferRelayers ?? false,
})
.accounts({
oracleQueue: oracleQueueAccount.publicKey,
@ -3217,25 +3222,13 @@ export class OracleAccount {
ProgramStateAccount.fromSeed(program);
const mint = await params.queueAccount.loadMint();
const wallet = await spl.createAccount(
program.provider.connection,
payerKeypair,
mint.address,
programWallet(program).publicKey,
Keypair.generate()
);
await spl.setAuthority(
program.provider.connection,
programWallet(program),
wallet,
programWallet(program),
spl.AuthorityType.AccountOwner,
programStateAccount.publicKey
);
const walletKeypair = Keypair.generate();
const [oracleAccount, oracleBump] = OracleAccount.fromSeed(
program,
params.queueAccount,
wallet
walletKeypair.publicKey
);
await program.methods
@ -3249,11 +3242,28 @@ export class OracleAccount {
oracle: oracleAccount.publicKey,
oracleAuthority: authorityKeypair.publicKey,
queue: params.queueAccount.publicKey,
wallet,
wallet: walletKeypair.publicKey,
programState: programStateAccount.publicKey,
systemProgram: SystemProgram.programId,
payer: programWallet(program).publicKey,
})
.preInstructions([
spl.createInitializeAccountInstruction(
walletKeypair.publicKey,
mint.address,
programWallet(program).publicKey,
spl.TOKEN_PROGRAM_ID
),
spl.createSetAuthorityInstruction(
walletKeypair.publicKey,
programWallet(program).publicKey,
spl.AuthorityType.AccountOwner,
programStateAccount.publicKey,
[programWallet(program), walletKeypair],
spl.TOKEN_PROGRAM_ID
),
])
.signers([walletKeypair])
.rpc();
return new OracleAccount({ program, publicKey: oracleAccount.publicKey });
@ -3583,24 +3593,15 @@ export class VrfAccount {
const keypair = params.keypair;
const size = program.account.vrfAccountData.size;
const switchTokenMint = await params.queue.loadMint();
const escrow = (
await spl.getOrCreateAssociatedTokenAccount(
program.provider.connection,
programWallet(program),
switchTokenMint.address,
keypair.publicKey,
true
)
).address;
await spl.setAuthority(
program.provider.connection,
programWallet(program),
escrow,
keypair,
spl.AuthorityType.AccountOwner,
programStateAccount.publicKey
const escrow = await spl.getAssociatedTokenAddress(
switchTokenMint.address,
keypair.publicKey,
true,
spl.TOKEN_PROGRAM_ID,
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
await program.methods
.vrfInit({
stateBump,
@ -3615,6 +3616,22 @@ export class VrfAccount {
tokenProgram: spl.TOKEN_PROGRAM_ID,
})
.preInstructions([
spl.createAssociatedTokenAccountInstruction(
programWallet(program).publicKey,
escrow,
keypair.publicKey,
switchTokenMint.address,
spl.TOKEN_PROGRAM_ID,
spl.ASSOCIATED_TOKEN_PROGRAM_ID
),
spl.createSetAuthorityInstruction(
escrow,
keypair.publicKey,
spl.AuthorityType.AccountOwner,
programStateAccount.publicKey,
[programWallet(program), keypair],
spl.TOKEN_PROGRAM_ID
),
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: keypair.publicKey,
@ -3632,21 +3649,6 @@ export class VrfAccount {
return new VrfAccount({ program, keypair, publicKey: keypair.publicKey });
}
/**
* Set the callback CPI when vrf verification is successful.
*/
// async setCallback(
// params: VrfSetCallbackParams
// ): Promise<TransactionSignature> {
// return await this.program.rpc.vrfSetCallback(params, {
// accounts: {
// vrf: this.publicKey,
// authority: params.authority.publicKey,
// },
// signers: [params.authority],
// });
// }
/**
* Trigger new randomness production on the vrf account
*/

View File

@ -35,7 +35,7 @@
"docs:deploy": "yarn workspace website deploy",
"gen:idl": "rawrtools gen:anchor SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f -o website/idl -p /idl",
"gen:idl:devnet": "rawrtools gen:anchor --devnet 2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG -o website/idl -p /idl",
"nuke": "shx rm -rf {./programs/*,./packages/*,./website,./libraries/*,.}/{node_modules,yarn*.log,build,dist,lib,.anchor,Cargo.lock,.docusaurus,target/release,target/rls, target/bpfel-unknown-unknown}"
"nuke": "shx rm -rf {./programs/*,./packages/*,./website,./libraries/*,.}/{node_modules,yarn*.log,build,dist,lib,.anchor,Cargo.lock,.docusaurus,target/release,target/rls,target/bpfel-unknown-unknown}"
},
"devDependencies": {
"@gallynaut/rawrtools": "^0.0.1",

View File

@ -12,7 +12,7 @@
"main": "dist/index.js",
"scripts": {
"start": "ts-node src/main",
"build": "rimraf dist && ./esbuild.js",
"build": "shx rm -rf dist && ./esbuild.js",
"test": "echo \"No test script for @switchboard-xyz/v2-feed-parser\" && exit 0"
},
"author": "",
@ -27,8 +27,8 @@
"@types/big.js": "^6.1.3",
"esbuild-node-externals": "^1.4.1",
"estrella": "^1.4.1",
"rimraf": "^3.0.2",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
"typescript": "^4.6.3",
"shx": "^0.3.4"
}
}

View File

@ -0,0 +1,22 @@
[package]
name = "anchor-buffer-parser"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "anchor_buffer_parser"
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
[dependencies]
switchboard-v2 = { path = "../../libraries/rs" }
# switchboard-v2 = "^0.1.11"
anchor-lang = "^0.24.2"
solana-program = "~1.9.13"

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,67 @@
import * as borsh from "@project-serum/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars
import { Connection, PublicKey } from "@solana/web3.js"
import { PROGRAM_ID } from "../programId"
export interface BufferClientFields {}
export interface BufferClientJSON {}
export class BufferClient {
static readonly discriminator = Buffer.from([142, 182, 89, 69, 9, 66, 10, 86])
static readonly layout = borsh.struct([])
constructor(fields: BufferClientFields) {}
static async fetch(
c: Connection,
address: PublicKey
): Promise<BufferClient | null> {
const info = await c.getAccountInfo(address)
if (info === null) {
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data)
}
static async fetchMultiple(
c: Connection,
addresses: PublicKey[]
): Promise<Array<BufferClient | null>> {
const infos = await c.getMultipleAccountsInfo(addresses)
return infos.map((info) => {
if (info === null) {
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data)
})
}
static decode(data: Buffer): BufferClient {
if (!data.slice(0, 8).equals(BufferClient.discriminator)) {
throw new Error("invalid account discriminator")
}
const dec = BufferClient.layout.decode(data.slice(8))
return new BufferClient({})
}
toJSON(): BufferClientJSON {
return {}
}
static fromJSON(obj: BufferClientJSON): BufferClient {
return new BufferClient({})
}
}

View File

@ -0,0 +1,5 @@
export {
BufferClient,
BufferClientFields,
BufferClientJSON,
} from "./BufferClient"

View File

@ -0,0 +1,690 @@
export type AnchorError =
| InstructionMissing
| InstructionFallbackNotFound
| InstructionDidNotDeserialize
| InstructionDidNotSerialize
| IdlInstructionStub
| IdlInstructionInvalidProgram
| ConstraintMut
| ConstraintHasOne
| ConstraintSigner
| ConstraintRaw
| ConstraintOwner
| ConstraintRentExempt
| ConstraintSeeds
| ConstraintExecutable
| ConstraintState
| ConstraintAssociated
| ConstraintAssociatedInit
| ConstraintClose
| ConstraintAddress
| ConstraintZero
| ConstraintTokenMint
| ConstraintTokenOwner
| ConstraintMintMintAuthority
| ConstraintMintFreezeAuthority
| ConstraintMintDecimals
| ConstraintSpace
| RequireViolated
| RequireEqViolated
| RequireKeysEqViolated
| RequireNeqViolated
| RequireKeysNeqViolated
| RequireGtViolated
| RequireGteViolated
| AccountDiscriminatorAlreadySet
| AccountDiscriminatorNotFound
| AccountDiscriminatorMismatch
| AccountDidNotDeserialize
| AccountDidNotSerialize
| AccountNotEnoughKeys
| AccountNotMutable
| AccountOwnedByWrongProgram
| InvalidProgramId
| InvalidProgramExecutable
| AccountNotSigner
| AccountNotSystemOwned
| AccountNotInitialized
| AccountNotProgramData
| AccountNotAssociatedTokenAccount
| AccountSysvarMismatch
| StateInvalidAddress
| DeclaredProgramIdMismatch
| Deprecated
export class InstructionMissing extends Error {
readonly code = 100
readonly name = "InstructionMissing"
readonly msg = "8 byte instruction identifier not provided"
constructor() {
super("100: 8 byte instruction identifier not provided")
}
}
export class InstructionFallbackNotFound extends Error {
readonly code = 101
readonly name = "InstructionFallbackNotFound"
readonly msg = "Fallback functions are not supported"
constructor() {
super("101: Fallback functions are not supported")
}
}
export class InstructionDidNotDeserialize extends Error {
readonly code = 102
readonly name = "InstructionDidNotDeserialize"
readonly msg = "The program could not deserialize the given instruction"
constructor() {
super("102: The program could not deserialize the given instruction")
}
}
export class InstructionDidNotSerialize extends Error {
readonly code = 103
readonly name = "InstructionDidNotSerialize"
readonly msg = "The program could not serialize the given instruction"
constructor() {
super("103: The program could not serialize the given instruction")
}
}
export class IdlInstructionStub extends Error {
readonly code = 1000
readonly name = "IdlInstructionStub"
readonly msg = "The program was compiled without idl instructions"
constructor() {
super("1000: The program was compiled without idl instructions")
}
}
export class IdlInstructionInvalidProgram extends Error {
readonly code = 1001
readonly name = "IdlInstructionInvalidProgram"
readonly msg =
"The transaction was given an invalid program for the IDL instruction"
constructor() {
super(
"1001: The transaction was given an invalid program for the IDL instruction"
)
}
}
export class ConstraintMut extends Error {
readonly code = 2000
readonly name = "ConstraintMut"
readonly msg = "A mut constraint was violated"
constructor() {
super("2000: A mut constraint was violated")
}
}
export class ConstraintHasOne extends Error {
readonly code = 2001
readonly name = "ConstraintHasOne"
readonly msg = "A has_one constraint was violated"
constructor() {
super("2001: A has_one constraint was violated")
}
}
export class ConstraintSigner extends Error {
readonly code = 2002
readonly name = "ConstraintSigner"
readonly msg = "A signer constraint was violated"
constructor() {
super("2002: A signer constraint was violated")
}
}
export class ConstraintRaw extends Error {
readonly code = 2003
readonly name = "ConstraintRaw"
readonly msg = "A raw constraint was violated"
constructor() {
super("2003: A raw constraint was violated")
}
}
export class ConstraintOwner extends Error {
readonly code = 2004
readonly name = "ConstraintOwner"
readonly msg = "An owner constraint was violated"
constructor() {
super("2004: An owner constraint was violated")
}
}
export class ConstraintRentExempt extends Error {
readonly code = 2005
readonly name = "ConstraintRentExempt"
readonly msg = "A rent exemption constraint was violated"
constructor() {
super("2005: A rent exemption constraint was violated")
}
}
export class ConstraintSeeds extends Error {
readonly code = 2006
readonly name = "ConstraintSeeds"
readonly msg = "A seeds constraint was violated"
constructor() {
super("2006: A seeds constraint was violated")
}
}
export class ConstraintExecutable extends Error {
readonly code = 2007
readonly name = "ConstraintExecutable"
readonly msg = "An executable constraint was violated"
constructor() {
super("2007: An executable constraint was violated")
}
}
export class ConstraintState extends Error {
readonly code = 2008
readonly name = "ConstraintState"
readonly msg = "A state constraint was violated"
constructor() {
super("2008: A state constraint was violated")
}
}
export class ConstraintAssociated extends Error {
readonly code = 2009
readonly name = "ConstraintAssociated"
readonly msg = "An associated constraint was violated"
constructor() {
super("2009: An associated constraint was violated")
}
}
export class ConstraintAssociatedInit extends Error {
readonly code = 2010
readonly name = "ConstraintAssociatedInit"
readonly msg = "An associated init constraint was violated"
constructor() {
super("2010: An associated init constraint was violated")
}
}
export class ConstraintClose extends Error {
readonly code = 2011
readonly name = "ConstraintClose"
readonly msg = "A close constraint was violated"
constructor() {
super("2011: A close constraint was violated")
}
}
export class ConstraintAddress extends Error {
readonly code = 2012
readonly name = "ConstraintAddress"
readonly msg = "An address constraint was violated"
constructor() {
super("2012: An address constraint was violated")
}
}
export class ConstraintZero extends Error {
readonly code = 2013
readonly name = "ConstraintZero"
readonly msg = "Expected zero account discriminant"
constructor() {
super("2013: Expected zero account discriminant")
}
}
export class ConstraintTokenMint extends Error {
readonly code = 2014
readonly name = "ConstraintTokenMint"
readonly msg = "A token mint constraint was violated"
constructor() {
super("2014: A token mint constraint was violated")
}
}
export class ConstraintTokenOwner extends Error {
readonly code = 2015
readonly name = "ConstraintTokenOwner"
readonly msg = "A token owner constraint was violated"
constructor() {
super("2015: A token owner constraint was violated")
}
}
export class ConstraintMintMintAuthority extends Error {
readonly code = 2016
readonly name = "ConstraintMintMintAuthority"
readonly msg = "A mint mint authority constraint was violated"
constructor() {
super("2016: A mint mint authority constraint was violated")
}
}
export class ConstraintMintFreezeAuthority extends Error {
readonly code = 2017
readonly name = "ConstraintMintFreezeAuthority"
readonly msg = "A mint freeze authority constraint was violated"
constructor() {
super("2017: A mint freeze authority constraint was violated")
}
}
export class ConstraintMintDecimals extends Error {
readonly code = 2018
readonly name = "ConstraintMintDecimals"
readonly msg = "A mint decimals constraint was violated"
constructor() {
super("2018: A mint decimals constraint was violated")
}
}
export class ConstraintSpace extends Error {
readonly code = 2019
readonly name = "ConstraintSpace"
readonly msg = "A space constraint was violated"
constructor() {
super("2019: A space constraint was violated")
}
}
export class RequireViolated extends Error {
readonly code = 2500
readonly name = "RequireViolated"
readonly msg = "A require expression was violated"
constructor() {
super("2500: A require expression was violated")
}
}
export class RequireEqViolated extends Error {
readonly code = 2501
readonly name = "RequireEqViolated"
readonly msg = "A require_eq expression was violated"
constructor() {
super("2501: A require_eq expression was violated")
}
}
export class RequireKeysEqViolated extends Error {
readonly code = 2502
readonly name = "RequireKeysEqViolated"
readonly msg = "A require_keys_eq expression was violated"
constructor() {
super("2502: A require_keys_eq expression was violated")
}
}
export class RequireNeqViolated extends Error {
readonly code = 2503
readonly name = "RequireNeqViolated"
readonly msg = "A require_neq expression was violated"
constructor() {
super("2503: A require_neq expression was violated")
}
}
export class RequireKeysNeqViolated extends Error {
readonly code = 2504
readonly name = "RequireKeysNeqViolated"
readonly msg = "A require_keys_neq expression was violated"
constructor() {
super("2504: A require_keys_neq expression was violated")
}
}
export class RequireGtViolated extends Error {
readonly code = 2505
readonly name = "RequireGtViolated"
readonly msg = "A require_gt expression was violated"
constructor() {
super("2505: A require_gt expression was violated")
}
}
export class RequireGteViolated extends Error {
readonly code = 2506
readonly name = "RequireGteViolated"
readonly msg = "A require_gte expression was violated"
constructor() {
super("2506: A require_gte expression was violated")
}
}
export class AccountDiscriminatorAlreadySet extends Error {
readonly code = 3000
readonly name = "AccountDiscriminatorAlreadySet"
readonly msg = "The account discriminator was already set on this account"
constructor() {
super("3000: The account discriminator was already set on this account")
}
}
export class AccountDiscriminatorNotFound extends Error {
readonly code = 3001
readonly name = "AccountDiscriminatorNotFound"
readonly msg = "No 8 byte discriminator was found on the account"
constructor() {
super("3001: No 8 byte discriminator was found on the account")
}
}
export class AccountDiscriminatorMismatch extends Error {
readonly code = 3002
readonly name = "AccountDiscriminatorMismatch"
readonly msg = "8 byte discriminator did not match what was expected"
constructor() {
super("3002: 8 byte discriminator did not match what was expected")
}
}
export class AccountDidNotDeserialize extends Error {
readonly code = 3003
readonly name = "AccountDidNotDeserialize"
readonly msg = "Failed to deserialize the account"
constructor() {
super("3003: Failed to deserialize the account")
}
}
export class AccountDidNotSerialize extends Error {
readonly code = 3004
readonly name = "AccountDidNotSerialize"
readonly msg = "Failed to serialize the account"
constructor() {
super("3004: Failed to serialize the account")
}
}
export class AccountNotEnoughKeys extends Error {
readonly code = 3005
readonly name = "AccountNotEnoughKeys"
readonly msg = "Not enough account keys given to the instruction"
constructor() {
super("3005: Not enough account keys given to the instruction")
}
}
export class AccountNotMutable extends Error {
readonly code = 3006
readonly name = "AccountNotMutable"
readonly msg = "The given account is not mutable"
constructor() {
super("3006: The given account is not mutable")
}
}
export class AccountOwnedByWrongProgram extends Error {
readonly code = 3007
readonly name = "AccountOwnedByWrongProgram"
readonly msg =
"The given account is owned by a different program than expected"
constructor() {
super(
"3007: The given account is owned by a different program than expected"
)
}
}
export class InvalidProgramId extends Error {
readonly code = 3008
readonly name = "InvalidProgramId"
readonly msg = "Program ID was not as expected"
constructor() {
super("3008: Program ID was not as expected")
}
}
export class InvalidProgramExecutable extends Error {
readonly code = 3009
readonly name = "InvalidProgramExecutable"
readonly msg = "Program account is not executable"
constructor() {
super("3009: Program account is not executable")
}
}
export class AccountNotSigner extends Error {
readonly code = 3010
readonly name = "AccountNotSigner"
readonly msg = "The given account did not sign"
constructor() {
super("3010: The given account did not sign")
}
}
export class AccountNotSystemOwned extends Error {
readonly code = 3011
readonly name = "AccountNotSystemOwned"
readonly msg = "The given account is not owned by the system program"
constructor() {
super("3011: The given account is not owned by the system program")
}
}
export class AccountNotInitialized extends Error {
readonly code = 3012
readonly name = "AccountNotInitialized"
readonly msg = "The program expected this account to be already initialized"
constructor() {
super("3012: The program expected this account to be already initialized")
}
}
export class AccountNotProgramData extends Error {
readonly code = 3013
readonly name = "AccountNotProgramData"
readonly msg = "The given account is not a program data account"
constructor() {
super("3013: The given account is not a program data account")
}
}
export class AccountNotAssociatedTokenAccount extends Error {
readonly code = 3014
readonly name = "AccountNotAssociatedTokenAccount"
readonly msg = "The given account is not the associated token account"
constructor() {
super("3014: The given account is not the associated token account")
}
}
export class AccountSysvarMismatch extends Error {
readonly code = 3015
readonly name = "AccountSysvarMismatch"
readonly msg = "The given public key does not match the required sysvar"
constructor() {
super("3015: The given public key does not match the required sysvar")
}
}
export class StateInvalidAddress extends Error {
readonly code = 4000
readonly name = "StateInvalidAddress"
readonly msg = "The given state account does not have the correct address"
constructor() {
super("4000: The given state account does not have the correct address")
}
}
export class DeclaredProgramIdMismatch extends Error {
readonly code = 4100
readonly name = "DeclaredProgramIdMismatch"
readonly msg = "The declared program id does not match the actual program id"
constructor() {
super("4100: The declared program id does not match the actual program id")
}
}
export class Deprecated extends Error {
readonly code = 5000
readonly name = "Deprecated"
readonly msg = "The API being used is deprecated and should no longer be used"
constructor() {
super("5000: The API being used is deprecated and should no longer be used")
}
}
export function fromCode(code: number): AnchorError | null {
switch (code) {
case 100:
return new InstructionMissing()
case 101:
return new InstructionFallbackNotFound()
case 102:
return new InstructionDidNotDeserialize()
case 103:
return new InstructionDidNotSerialize()
case 1000:
return new IdlInstructionStub()
case 1001:
return new IdlInstructionInvalidProgram()
case 2000:
return new ConstraintMut()
case 2001:
return new ConstraintHasOne()
case 2002:
return new ConstraintSigner()
case 2003:
return new ConstraintRaw()
case 2004:
return new ConstraintOwner()
case 2005:
return new ConstraintRentExempt()
case 2006:
return new ConstraintSeeds()
case 2007:
return new ConstraintExecutable()
case 2008:
return new ConstraintState()
case 2009:
return new ConstraintAssociated()
case 2010:
return new ConstraintAssociatedInit()
case 2011:
return new ConstraintClose()
case 2012:
return new ConstraintAddress()
case 2013:
return new ConstraintZero()
case 2014:
return new ConstraintTokenMint()
case 2015:
return new ConstraintTokenOwner()
case 2016:
return new ConstraintMintMintAuthority()
case 2017:
return new ConstraintMintFreezeAuthority()
case 2018:
return new ConstraintMintDecimals()
case 2019:
return new ConstraintSpace()
case 2500:
return new RequireViolated()
case 2501:
return new RequireEqViolated()
case 2502:
return new RequireKeysEqViolated()
case 2503:
return new RequireNeqViolated()
case 2504:
return new RequireKeysNeqViolated()
case 2505:
return new RequireGtViolated()
case 2506:
return new RequireGteViolated()
case 3000:
return new AccountDiscriminatorAlreadySet()
case 3001:
return new AccountDiscriminatorNotFound()
case 3002:
return new AccountDiscriminatorMismatch()
case 3003:
return new AccountDidNotDeserialize()
case 3004:
return new AccountDidNotSerialize()
case 3005:
return new AccountNotEnoughKeys()
case 3006:
return new AccountNotMutable()
case 3007:
return new AccountOwnedByWrongProgram()
case 3008:
return new InvalidProgramId()
case 3009:
return new InvalidProgramExecutable()
case 3010:
return new AccountNotSigner()
case 3011:
return new AccountNotSystemOwned()
case 3012:
return new AccountNotInitialized()
case 3013:
return new AccountNotProgramData()
case 3014:
return new AccountNotAssociatedTokenAccount()
case 3015:
return new AccountSysvarMismatch()
case 4000:
return new StateInvalidAddress()
case 4100:
return new DeclaredProgramIdMismatch()
case 5000:
return new Deprecated()
}
return null
}

View File

@ -0,0 +1,63 @@
export type CustomError =
| InvalidSwitchboardAccount
| ExpectedResultMismatch
| StaleBuffer
| StringConversionFailed
export class InvalidSwitchboardAccount extends Error {
readonly code = 6000
readonly name = "InvalidSwitchboardAccount"
readonly msg = "Not a valid Switchboard account"
constructor() {
super("6000: Not a valid Switchboard account")
}
}
export class ExpectedResultMismatch extends Error {
readonly code = 6001
readonly name = "ExpectedResultMismatch"
readonly msg = "Switchboard buffer does not match provided expected_result!"
constructor() {
super("6001: Switchboard buffer does not match provided expected_result!")
}
}
export class StaleBuffer extends Error {
readonly code = 6002
readonly name = "StaleBuffer"
readonly msg =
"Switchboard buffer has not been updated in the last 5 minutes!"
constructor() {
super(
"6002: Switchboard buffer has not been updated in the last 5 minutes!"
)
}
}
export class StringConversionFailed extends Error {
readonly code = 6003
readonly name = "StringConversionFailed"
readonly msg = "Failed to convert the buffer to a string!"
constructor() {
super("6003: Failed to convert the buffer to a string!")
}
}
export function fromCode(code: number): CustomError | null {
switch (code) {
case 6000:
return new InvalidSwitchboardAccount()
case 6001:
return new ExpectedResultMismatch()
case 6002:
return new StaleBuffer()
case 6003:
return new StringConversionFailed()
}
return null
}

View File

@ -0,0 +1,57 @@
import { PROGRAM_ID } from "../programId"
import * as anchor from "./anchor"
import * as custom from "./custom"
export function fromCode(
code: number
): custom.CustomError | anchor.AnchorError | null {
return code >= 6000 ? custom.fromCode(code) : anchor.fromCode(code)
}
function hasOwnProperty<X extends object, Y extends PropertyKey>(
obj: X,
prop: Y
): obj is X & Record<Y, unknown> {
return Object.hasOwnProperty.call(obj, prop)
}
const errorRe = /Program (\w+) failed: custom program error: (\w+)/
export function fromTxError(
err: unknown
): custom.CustomError | anchor.AnchorError | null {
if (
typeof err !== "object" ||
err === null ||
!hasOwnProperty(err, "logs") ||
!Array.isArray(err.logs)
) {
return null
}
let firstMatch: RegExpExecArray | null = null
for (const logLine of err.logs) {
firstMatch = errorRe.exec(logLine)
if (firstMatch !== null) {
break
}
}
if (firstMatch === null) {
return null
}
const [programIdRaw, codeRaw] = firstMatch.slice(1)
if (programIdRaw !== PROGRAM_ID.toString()) {
return null
}
let errorCode: number
try {
errorCode = parseInt(codeRaw, 16)
} catch (parseErr) {
return null
}
return fromCode(errorCode)
}

View File

@ -0,0 +1 @@
export { readResult, ReadResultAccounts, ReadResultArgs } from "./readResult"

View File

@ -0,0 +1,29 @@
import * as borsh from "@project-serum/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars
import { PublicKey, TransactionInstruction } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars
import { PROGRAM_ID } from "../programId"
import * as types from "../types" // eslint-disable-line @typescript-eslint/no-unused-vars
export interface ReadResultArgs {
params: types.ReadResultParamsFields
}
export interface ReadResultAccounts {
buffer: PublicKey
}
export const layout = borsh.struct([types.ReadResultParams.layout("params")])
export function readResult(args: ReadResultArgs, accounts: ReadResultAccounts) {
const keys = [{ pubkey: accounts.buffer, isSigner: false, isWritable: false }]
const identifier = Buffer.from([130, 229, 115, 203, 180, 191, 240, 90])
const buffer = Buffer.alloc(1000)
const len = layout.encode(
{
params: types.ReadResultParams.toEncodable(args.params),
},
buffer
)
const data = Buffer.concat([identifier, buffer]).slice(0, 8 + len)
const ix = new TransactionInstruction({ keys, programId: PROGRAM_ID, data })
return ix
}

View File

@ -0,0 +1,9 @@
import { PublicKey } from "@solana/web3.js"
// Program ID passed with the cli --program-id flag when running the code generator. Do not edit, it will get overwritten.
export const PROGRAM_ID_CLI = new PublicKey(
"96punQGZDShZGkzsBa3SsfTxfUnwu4XGpzXbhF7NTgcP"
)
// This constant will not get overwritten on subsequent code generations and it's safe to modify it's value.
export const PROGRAM_ID: PublicKey = PROGRAM_ID_CLI

View File

@ -0,0 +1,55 @@
import * as borsh from "@project-serum/borsh"
export interface ReadResultParamsFields {
expectedResult: Array<number> | null
}
export interface ReadResultParamsJSON {
expectedResult: Array<number> | null
}
export class ReadResultParams {
readonly expectedResult: Array<number> | null
constructor(fields: ReadResultParamsFields) {
this.expectedResult = fields.expectedResult
}
static layout(property?: string) {
return borsh.struct(
[borsh.option(borsh.vecU8(), "expectedResult")],
property
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static fromDecoded(obj: any) {
return new ReadResultParams({
expectedResult:
(obj.expectedResult && Array.from(obj.expectedResult)) || null,
})
}
static toEncodable(fields: ReadResultParamsFields) {
return {
expectedResult:
(fields.expectedResult && Buffer.from(fields.expectedResult)) || null,
}
}
toJSON(): ReadResultParamsJSON {
return {
expectedResult: this.expectedResult,
}
}
static fromJSON(obj: ReadResultParamsJSON): ReadResultParams {
return new ReadResultParams({
expectedResult: obj.expectedResult,
})
}
toEncodable() {
return ReadResultParams.toEncodable(this)
}
}

View File

@ -0,0 +1,5 @@
export {
ReadResultParams,
ReadResultParamsFields,
ReadResultParamsJSON,
} from "./ReadResultParams"

View File

@ -0,0 +1,32 @@
{
"name": "anchor-buffer-parser",
"version": "1.0.0",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/switchboard-xyz/switchboard-v2",
"directory": "programs/anchor-buffer-parser"
},
"scripts": {
"build": "echo \"For workspace anchor-buffer-parser, run 'anchor build' from the project root\" && exit 0",
"lint": "eslint --ext .js,.json,.ts 'src/**' --fix"
},
"dependencies": {
"@project-serum/anchor": "^0.24.2",
"@solana/web3.js": "^1.42.0",
"@switchboard-xyz/sbv2-utils": "^0.1.33",
"@switchboard-xyz/switchboard-v2": "^0.0.121",
"node-fetch": "^2.6"
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^17.0.45",
"@types/node-fetch": "^2.6.2",
"chai": "^4.3.6",
"mocha": "^9.0.3",
"ts-mocha": "^9.0.2",
"ts-node": "^10.4.0",
"typescript": "^4.3.5"
}
}

View File

@ -0,0 +1,88 @@
#[allow(unaligned_references)]
use anchor_lang::prelude::*;
use anchor_lang::solana_program::clock;
pub use switchboard_v2::{BufferRelayerAccountData, SWITCHBOARD_V2_DEVNET, SWITCHBOARD_V2_MAINNET};
declare_id!("96punQGZDShZGkzsBa3SsfTxfUnwu4XGpzXbhF7NTgcP");
#[account(zero_copy)]
#[derive(AnchorDeserialize, Debug)]
pub struct BufferClient {}
#[derive(Accounts)]
#[instruction(params: ReadResultParams)]
pub struct ReadResult<'info> {
/// CHECK:
pub buffer: AccountInfo<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct ReadResultParams {
pub expected_result: Option<Vec<u8>>,
}
#[program]
pub mod anchor_buffer_parser {
use super::*;
pub fn read_result(
ctx: Context<ReadResult>,
params: ReadResultParams,
) -> anchor_lang::Result<()> {
let buffer_account = &ctx.accounts.buffer;
// check buffer owner
let owner = *buffer_account.owner;
if owner != SWITCHBOARD_V2_DEVNET && owner != SWITCHBOARD_V2_MAINNET {
msg!("Feed owner = {:?}", owner);
return Err(error!(BufferErrorCode::InvalidSwitchboardAccount));
}
// load and deserialize buffer
let buffer = BufferRelayerAccountData::new(buffer_account)?;
msg!("Buffer account loaded!");
let buffer_result = buffer.get_result();
// get result
msg!("Current buffer result is {:?}!", buffer_result);
// check whether the buffer has been updated in the last 300 seconds
buffer
.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)
.map_err(|_| error!(BufferErrorCode::StaleBuffer))?;
// compare buffer with expected result
if let Some(expected_result) = params.expected_result {
if expected_result != *buffer_result {
msg!(
"Buffer mismatch, expected {:?}, actual {:?}",
expected_result,
buffer_result
);
return Err(error!(BufferErrorCode::ExpectedResultMismatch));
}
}
let result_string = String::from_utf8(buffer.result)
.map_err(|_| error!(BufferErrorCode::StringConversionFailed))?;
msg!("Buffer string {:?}!", result_string);
Ok(())
}
}
#[error_code]
#[derive(Eq, PartialEq)]
pub enum BufferErrorCode {
#[msg("Not a valid Switchboard account")]
InvalidSwitchboardAccount,
#[msg("Switchboard buffer does not match provided expected_result!")]
ExpectedResultMismatch,
#[msg("Switchboard buffer has not been updated in the last 5 minutes!")]
StaleBuffer,
#[msg("Failed to convert the buffer to a string!")]
StringConversionFailed,
}

View File

@ -0,0 +1,198 @@
import * as anchor from "@project-serum/anchor";
import {
promiseWithTimeout,
SwitchboardTestContext,
} from "@switchboard-xyz/sbv2-utils";
import {
AnchorWallet,
BufferRelayerAccount,
JobAccount,
OracleJob,
PermissionAccount,
} from "@switchboard-xyz/switchboard-v2";
import fetch from "node-fetch";
import {
AnchorBufferParser,
IDL,
} from "../../../target/types/anchor_buffer_parser";
import { PROGRAM_ID } from "../client/programId";
const sleep = (ms: number): Promise<any> =>
new Promise((s) => setTimeout(s, ms));
// Anchor.toml will copy this to localnet when we start our tests
describe("anchor-buffer-parser test", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// const bufferParserProgram = anchor.workspace
// .AnchorBufferParser as Program<AnchorBufferParser>;
const bufferParserProgram = new anchor.Program(
IDL,
PROGRAM_ID,
provider,
new anchor.BorshCoder(IDL)
) as anchor.Program<AnchorBufferParser>;
const payer = (provider.wallet as AnchorWallet).payer;
let switchboard: SwitchboardTestContext;
let localnet = false;
before(async () => {
// First, attempt to load the switchboard devnet PID
try {
switchboard = await SwitchboardTestContext.loadDevnetQueue(
provider,
"9e8rQPoyVZeGA1mD2BBVqDthbFwircnfLTB5JE6yUFQR"
);
console.log("devnet detected");
return;
} catch (error: any) {
console.log(`Error: SBV2 Devnet - ${error.message}`);
}
// If fails, fallback to looking for a local env file
try {
switchboard = await SwitchboardTestContext.loadFromEnv(provider);
localnet = true;
console.log("localnet detected");
return;
} catch (error: any) {
console.log(`Error: SBV2 Localnet - ${error.message}`);
}
// If fails, throw error
throw new Error(
`Failed to load the SwitchboardTestContext from devnet or from a switchboard.env file`
);
});
it("Create and read buffer account", async () => {
const queue = await switchboard.queue.loadData();
if (!queue.enableBufferRelayers) {
throw new Error(`Queue has buffer relayers disabled!`);
}
const url = "https://jsonplaceholder.typicode.com/todos/1";
const expectedResult = Buffer.from(await (await fetch(url)).text());
const jobData = Buffer.from(
OracleJob.encodeDelimited(
OracleJob.create({
tasks: [
OracleJob.Task.create({
httpTask: OracleJob.HttpTask.create({
url,
}),
}),
],
})
).finish()
);
const jobKeypair = anchor.web3.Keypair.generate();
const jobAccount = await JobAccount.create(switchboard.program, {
data: jobData,
keypair: jobKeypair,
authority: payer.publicKey,
});
const bufferAccount = await BufferRelayerAccount.create(
switchboard.program,
{
name: Buffer.from("My Buffer").slice(0, 32),
minUpdateDelaySeconds: 30,
queueAccount: switchboard.queue,
authority: payer.publicKey,
jobAccount,
}
);
console.log(`BufferRelayer ${bufferAccount.publicKey}`);
const permissionAccount = await PermissionAccount.create(
switchboard.program,
{
granter: switchboard.queue.publicKey,
grantee: bufferAccount.publicKey,
authority: queue.authority,
}
);
// call openRound
bufferAccount
.openRound()
.then((sig) => console.log(`OpenRound Txn: ${sig}`));
const buf = await awaitCallback(bufferAccount, 30_000);
console.log(`Current Buffer Result: [${new Uint8Array(buf).toString()}]`);
const signature = await bufferParserProgram.methods
.readResult({ expectedResult: expectedResult })
.accounts({ buffer: bufferAccount.publicKey })
.rpc()
.catch((error) => {
console.error(error);
throw error;
});
await sleep(2000);
const logs = await provider.connection.getParsedTransaction(
signature,
"confirmed"
);
console.log(JSON.stringify(logs?.meta?.logMessages, undefined, 2));
});
});
async function awaitCallback(
bufferAccount: BufferRelayerAccount,
timeoutInterval: number,
errorMsg = "Timed out waiting for Buffer Relayer open round."
) {
const acctCoder = new anchor.BorshAccountsCoder(bufferAccount.program.idl);
let ws: number | undefined = undefined;
const result: Buffer = await promiseWithTimeout(
timeoutInterval,
new Promise(
(resolve: (result: Buffer) => void, reject: (reason: string) => void) => {
ws = bufferAccount.program.provider.connection.onAccountChange(
bufferAccount.publicKey,
async (
accountInfo: anchor.web3.AccountInfo<Buffer>,
context: anchor.web3.Context
) => {
const buf = acctCoder.decode(
"BufferRelayerAccountData",
accountInfo.data
);
const bufResult = buf.result as Buffer;
if (bufResult.byteLength > 0) {
resolve(bufResult);
}
}
);
}
).finally(async () => {
if (ws) {
await bufferAccount.program.provider.connection.removeAccountChangeListener(
ws
);
}
ws = undefined;
}),
new Error(errorMsg)
).finally(async () => {
if (ws) {
await bufferAccount.program.provider.connection.removeAccountChangeListener(
ws
);
}
ws = undefined;
});
return result;
}

View File

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"module": "CommonJS",
"noEmit": true,
"esModuleInterop": true,
"paths": {
"@switchboard-xyz/switchboard-v2": ["../../libraries/ts"]
}
},
"include": [
"tests/**/*",
"client/**/*",
"../../target/types/anchor_feed_parser"
],
"references": [{ "path": "../../libraries/ts" }]
}

View File

@ -17,6 +17,6 @@ default = []
[dependencies]
switchboard-v2 = { path = "../../libraries/rs" }
# switchboard-v2 = "^0.1.10"
# switchboard-v2 = "^0.1.11"
anchor-lang = "^0.24.2"
solana-program = "~1.9.13"

View File

@ -0,0 +1,69 @@
import * as borsh from "@project-serum/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars
import { Connection, PublicKey } from "@solana/web3.js"
import { PROGRAM_ID } from "../programId"
export interface FeedClientFields {}
export interface FeedClientJSON {}
export class FeedClient {
static readonly discriminator = Buffer.from([
30, 15, 152, 236, 85, 180, 84, 151,
])
static readonly layout = borsh.struct([])
constructor(fields: FeedClientFields) {}
static async fetch(
c: Connection,
address: PublicKey
): Promise<FeedClient | null> {
const info = await c.getAccountInfo(address)
if (info === null) {
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data)
}
static async fetchMultiple(
c: Connection,
addresses: PublicKey[]
): Promise<Array<FeedClient | null>> {
const infos = await c.getMultipleAccountsInfo(addresses)
return infos.map((info) => {
if (info === null) {
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data)
})
}
static decode(data: Buffer): FeedClient {
if (!data.slice(0, 8).equals(FeedClient.discriminator)) {
throw new Error("invalid account discriminator")
}
const dec = FeedClient.layout.decode(data.slice(8))
return new FeedClient({})
}
toJSON(): FeedClientJSON {
return {}
}
static fromJSON(obj: FeedClientJSON): FeedClient {
return new FeedClient({})
}
}

View File

@ -0,0 +1 @@
export { FeedClient, FeedClientFields, FeedClientJSON } from "./FeedClient"

View File

@ -0,0 +1,47 @@
export type CustomError =
| InvalidSwitchboardAccount
| StaleFeed
| ConfidenceIntervalExceeded
export class InvalidSwitchboardAccount extends Error {
readonly code = 6000
readonly name = "InvalidSwitchboardAccount"
readonly msg = "Not a valid Switchboard account"
constructor() {
super("6000: Not a valid Switchboard account")
}
}
export class StaleFeed extends Error {
readonly code = 6001
readonly name = "StaleFeed"
readonly msg = "Switchboard feed has not been updated in 5 minutes"
constructor() {
super("6001: Switchboard feed has not been updated in 5 minutes")
}
}
export class ConfidenceIntervalExceeded extends Error {
readonly code = 6002
readonly name = "ConfidenceIntervalExceeded"
readonly msg = "Switchboard feed exceeded provided confidence interval"
constructor() {
super("6002: Switchboard feed exceeded provided confidence interval")
}
}
export function fromCode(code: number): CustomError | null {
switch (code) {
case 6000:
return new InvalidSwitchboardAccount()
case 6001:
return new StaleFeed()
case 6002:
return new ConfidenceIntervalExceeded()
}
return null
}

View File

@ -1,8 +1,11 @@
import { PROGRAM_ID } from "../programId"
import * as anchor from "./anchor"
import * as custom from "./custom"
export function fromCode(code: number): anchor.AnchorError | null {
return anchor.fromCode(code)
export function fromCode(
code: number
): custom.CustomError | anchor.AnchorError | null {
return code >= 6000 ? custom.fromCode(code) : anchor.fromCode(code)
}
function hasOwnProperty<X extends object, Y extends PropertyKey>(
@ -14,7 +17,9 @@ function hasOwnProperty<X extends object, Y extends PropertyKey>(
const errorRe = /Program (\w+) failed: custom program error: (\w+)/
export function fromTxError(err: unknown): anchor.AnchorError | null {
export function fromTxError(
err: unknown
): custom.CustomError | anchor.AnchorError | null {
if (
typeof err !== "object" ||
err === null ||

View File

@ -1 +1 @@
export { readResult, ReadResultAccounts } from "./readResult"
export { readResult, ReadResultAccounts, ReadResultArgs } from "./readResult"

View File

@ -1,16 +1,31 @@
import * as borsh from "@project-serum/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars
import { PublicKey, TransactionInstruction } from "@solana/web3.js" // eslint-disable-line @typescript-eslint/no-unused-vars
import { PROGRAM_ID } from "../programId"
import * as types from "../types" // eslint-disable-line @typescript-eslint/no-unused-vars
export interface ReadResultArgs {
params: types.ReadResultParamsFields
}
export interface ReadResultAccounts {
aggregator: PublicKey
}
export function readResult(accounts: ReadResultAccounts) {
export const layout = borsh.struct([types.ReadResultParams.layout("params")])
export function readResult(args: ReadResultArgs, accounts: ReadResultAccounts) {
const keys = [
{ pubkey: accounts.aggregator, isSigner: false, isWritable: false },
]
const identifier = Buffer.from([130, 229, 115, 203, 180, 191, 240, 90])
const data = identifier
const buffer = Buffer.alloc(1000)
const len = layout.encode(
{
params: types.ReadResultParams.toEncodable(args.params),
},
buffer
)
const data = Buffer.concat([identifier, buffer]).slice(0, 8 + len)
const ix = new TransactionInstruction({ keys, programId: PROGRAM_ID, data })
return ix
}

View File

@ -1,5 +1,10 @@
import { PublicKey } from "@solana/web3.js"
// Program ID defined in the provided IDL. Do not edit, it will get overwritten.
export const PROGRAM_ID_IDL = new PublicKey(
"FnsPs665aBSwJRu2A8wGv6ZT76ipR41kHm4hoA3B1QGh"
)
// Program ID passed with the cli --program-id flag when running the code generator. Do not edit, it will get overwritten.
export const PROGRAM_ID_CLI = new PublicKey(
"FnsPs665aBSwJRu2A8wGv6ZT76ipR41kHm4hoA3B1QGh"

View File

@ -0,0 +1,53 @@
import * as borsh from "@project-serum/borsh"
export interface ReadResultParamsFields {
maxConfidenceInterval: number | null
}
export interface ReadResultParamsJSON {
maxConfidenceInterval: number | null
}
export class ReadResultParams {
readonly maxConfidenceInterval: number | null
constructor(fields: ReadResultParamsFields) {
this.maxConfidenceInterval = fields.maxConfidenceInterval
}
static layout(property?: string) {
return borsh.struct(
[borsh.option(borsh.f64(), "maxConfidenceInterval")],
property
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static fromDecoded(obj: any) {
return new ReadResultParams({
maxConfidenceInterval: obj.maxConfidenceInterval,
})
}
static toEncodable(fields: ReadResultParamsFields) {
return {
maxConfidenceInterval: fields.maxConfidenceInterval,
}
}
toJSON(): ReadResultParamsJSON {
return {
maxConfidenceInterval: this.maxConfidenceInterval,
}
}
static fromJSON(obj: ReadResultParamsJSON): ReadResultParams {
return new ReadResultParams({
maxConfidenceInterval: obj.maxConfidenceInterval,
})
}
toEncodable() {
return ReadResultParams.toEncodable(this)
}
}

View File

@ -0,0 +1,5 @@
export {
ReadResultParams,
ReadResultParamsFields,
ReadResultParamsJSON,
} from "./ReadResultParams"

View File

@ -14,8 +14,8 @@
"dependencies": {
"@project-serum/anchor": "^0.24.2",
"@solana/web3.js": "^1.42.0",
"@switchboard-xyz/sbv2-utils": "^0.1.32",
"@switchboard-xyz/switchboard-v2": "^0.0.120"
"@switchboard-xyz/sbv2-utils": "^0.1.33",
"@switchboard-xyz/switchboard-v2": "^0.0.121"
},
"devDependencies": {
"@types/chai": "^4.3.0",

View File

@ -2,7 +2,9 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::clock;
use std::convert::TryInto;
pub use switchboard_v2::{AggregatorAccountData, SWITCHBOARD_V2_DEVNET, SWITCHBOARD_V2_MAINNET};
pub use switchboard_v2::{
AggregatorAccountData, SwitchboardDecimal, SWITCHBOARD_V2_DEVNET, SWITCHBOARD_V2_MAINNET,
};
declare_id!("FnsPs665aBSwJRu2A8wGv6ZT76ipR41kHm4hoA3B1QGh");
@ -11,37 +13,49 @@ declare_id!("FnsPs665aBSwJRu2A8wGv6ZT76ipR41kHm4hoA3B1QGh");
pub struct FeedClient {}
#[derive(Accounts)]
#[instruction(params: ReadResultParams)]
pub struct ReadResult<'info> {
/// CHECK:
pub aggregator: AccountInfo<'info>,
}
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct ReadResultParams {
pub max_confidence_interval: Option<f64>,
}
#[program]
pub mod anchor_feed_parser {
use super::*;
pub fn read_result(ctx: Context<ReadResult>) -> anchor_lang::Result<()> {
pub fn read_result(
ctx: Context<ReadResult>,
params: ReadResultParams,
) -> anchor_lang::Result<()> {
let aggregator = &ctx.accounts.aggregator;
// check feed owner
let owner = *aggregator.owner;
if owner != SWITCHBOARD_V2_DEVNET && owner != SWITCHBOARD_V2_MAINNET {
return Err(error!(FeedErrorCode::InvalidSwitchboardVrfAccount));
return Err(error!(FeedErrorCode::InvalidSwitchboardAccount));
}
// load and deserialize feed
let feed = AggregatorAccountData::new(aggregator)?;
// check if feed has updated in the last 5 minutes
let staleness = clock::Clock::get().unwrap().unix_timestamp
- feed.latest_confirmed_round.round_open_timestamp;
if staleness > 300 {
msg!("Feed has not been updated in {} seconds!", staleness);
return Err(error!(FeedErrorCode::StaleFeed));
}
// get result
let val: f64 = feed.get_result()?.try_into()?;
// check whether the feed has been updated in the last 300 seconds
feed.check_staleness(clock::Clock::get().unwrap().unix_timestamp, 300)
.map_err(|_| error!(FeedErrorCode::StaleFeed))?;
// check feed does not exceed max_confidence_interval
if let Some(max_confidence_interval) = params.max_confidence_interval {
feed.check_confidence_interval(SwitchboardDecimal::from_f64(max_confidence_interval))
.map_err(|_| error!(FeedErrorCode::ConfidenceIntervalExceeded))?;
}
msg!("Current feed result is {}!", val);
Ok(())
@ -51,8 +65,10 @@ pub mod anchor_feed_parser {
#[error_code]
#[derive(Eq, PartialEq)]
pub enum FeedErrorCode {
#[msg("Not a valid Switchboard VRF account")]
InvalidSwitchboardVrfAccount,
#[msg("Not a valid Switchboard account")]
InvalidSwitchboardAccount,
#[msg("Switchboard feed has not been updated in 5 minutes")]
StaleFeed,
#[msg("Switchboard feed exceeded provided confidence interval")]
ConfidenceIntervalExceeded,
}

View File

@ -2,6 +2,7 @@ import * as anchor from "@project-serum/anchor";
import { PublicKey } from "@solana/web3.js";
import { SwitchboardTestContext } from "@switchboard-xyz/sbv2-utils";
import type { AnchorWallet } from "@switchboard-xyz/switchboard-v2";
import assert from "assert";
import {
AnchorFeedParser,
IDL,
@ -65,7 +66,7 @@ describe("anchor-feed-parser test", () => {
it("Read SOL/USD Feed", async () => {
const signature = await feedParserProgram.methods
.readResult()
.readResult({ maxConfidenceInterval: 0.25 })
.accounts({ aggregator: aggregatorKey })
.rpc();
@ -79,4 +80,18 @@ describe("anchor-feed-parser test", () => {
console.log(JSON.stringify(logs?.meta?.logMessages, undefined, 2));
});
it("Fails to read feed if confidence interval is exceeded", async () => {
await assert
.rejects(async function () {
await feedParserProgram.methods
.readResult({ maxConfidenceInterval: 0.0000000001 })
.accounts({ aggregator: aggregatorKey })
.rpc();
})
.catch((err) => {
console.error(err);
throw err;
});
});
});

View File

@ -17,7 +17,7 @@ default = []
[dependencies]
switchboard-v2 = { path = "../../libraries/rs" }
# switchboard-v2 = "^0.1.10"
# switchboard-v2 = "^0.1.11"
anchor-lang = "^0.24.2"
anchor-spl = "^0.24.2"
solana-program = "~1.9.13"

View File

@ -1,46 +1,40 @@
import * as borsh from "@project-serum/borsh"; // eslint-disable-line @typescript-eslint/no-unused-vars
import { Connection, PublicKey } from "@solana/web3.js";
import BN from "bn.js"; // eslint-disable-line @typescript-eslint/no-unused-vars
import { PROGRAM_ID } from "../programId";
import * as borsh from "@project-serum/borsh" // eslint-disable-line @typescript-eslint/no-unused-vars
import { Connection, PublicKey } from "@solana/web3.js"
import BN from "bn.js" // eslint-disable-line @typescript-eslint/no-unused-vars
import { PROGRAM_ID } from "../programId"
export interface VrfClientFields {
bump: number;
maxResult: BN;
resultBuffer: Array<number>;
result: BN;
lastTimestamp: BN;
authority: PublicKey;
vrf: PublicKey;
bump: number
maxResult: BN
resultBuffer: Array<number>
result: BN
lastTimestamp: BN
authority: PublicKey
vrf: PublicKey
}
export interface VrfClientJSON {
bump: number;
maxResult: string;
resultBuffer: Array<number>;
result: string;
lastTimestamp: string;
authority: string;
vrf: string;
bump: number
maxResult: string
resultBuffer: Array<number>
result: string
lastTimestamp: string
authority: string
vrf: string
}
export class VrfClient {
readonly bump: number;
readonly maxResult: BN;
readonly resultBuffer: Array<number>;
readonly result: BN;
readonly lastTimestamp: BN;
readonly authority: PublicKey;
readonly vrf: PublicKey;
readonly bump: number
readonly maxResult: BN
readonly resultBuffer: Array<number>
readonly result: BN
readonly lastTimestamp: BN
readonly authority: PublicKey
readonly vrf: PublicKey
static readonly discriminator = Buffer.from([
230, 174, 157, 153, 51, 18, 230, 163,
]);
])
static readonly layout = borsh.struct([
borsh.u8("bump"),
@ -50,58 +44,58 @@ export class VrfClient {
borsh.i64("lastTimestamp"),
borsh.publicKey("authority"),
borsh.publicKey("vrf"),
]);
])
constructor(fields: VrfClientFields) {
this.bump = fields.bump;
this.maxResult = fields.maxResult;
this.resultBuffer = fields.resultBuffer;
this.result = fields.result;
this.lastTimestamp = fields.lastTimestamp;
this.authority = fields.authority;
this.vrf = fields.vrf;
this.bump = fields.bump
this.maxResult = fields.maxResult
this.resultBuffer = fields.resultBuffer
this.result = fields.result
this.lastTimestamp = fields.lastTimestamp
this.authority = fields.authority
this.vrf = fields.vrf
}
static async fetch(
c: Connection,
address: PublicKey
): Promise<VrfClient | null> {
const info = await c.getAccountInfo(address);
const info = await c.getAccountInfo(address)
if (info === null) {
return null;
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program");
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data);
return this.decode(info.data)
}
static async fetchMultiple(
c: Connection,
addresses: PublicKey[]
): Promise<Array<VrfClient | null>> {
const infos = await c.getMultipleAccountsInfo(addresses);
const infos = await c.getMultipleAccountsInfo(addresses)
return infos.map((info) => {
if (info === null) {
return null;
return null
}
if (!info.owner.equals(PROGRAM_ID)) {
throw new Error("account doesn't belong to this program");
throw new Error("account doesn't belong to this program")
}
return this.decode(info.data);
});
return this.decode(info.data)
})
}
static decode(data: Buffer): VrfClient {
if (!data.slice(0, 8).equals(VrfClient.discriminator)) {
throw new Error("invalid account discriminator");
throw new Error("invalid account discriminator")
}
const dec = VrfClient.layout.decode(data.slice(8));
const dec = VrfClient.layout.decode(data.slice(8))
return new VrfClient({
bump: dec.bump,
@ -111,7 +105,7 @@ export class VrfClient {
lastTimestamp: dec.lastTimestamp,
authority: dec.authority,
vrf: dec.vrf,
});
})
}
toJSON(): VrfClientJSON {
@ -123,7 +117,7 @@ export class VrfClient {
lastTimestamp: this.lastTimestamp.toString(),
authority: this.authority.toString(),
vrf: this.vrf.toString(),
};
}
}
static fromJSON(obj: VrfClientJSON): VrfClient {
@ -135,6 +129,6 @@ export class VrfClient {
lastTimestamp: new BN(obj.lastTimestamp),
authority: new PublicKey(obj.authority),
vrf: new PublicKey(obj.vrf),
});
})
}
}

View File

@ -1,9 +1,9 @@
import { PublicKey } from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js"
// Program ID passed with the cli --program-id flag when running the code generator. Do not edit, it will get overwritten.
export const PROGRAM_ID_CLI = new PublicKey(
"HjjRFjCyQH3ne6Gg8Yn3TQafrrYecRrphwLwnh2A26vM"
);
)
// This constant will not get overwritten on subsequent code generations and it's safe to modify it's value.
export const PROGRAM_ID: PublicKey = PROGRAM_ID_CLI;
export const PROGRAM_ID: PublicKey = PROGRAM_ID_CLI

View File

@ -19,8 +19,8 @@
"@project-serum/borsh": "^0.2.5",
"@solana/spl-token": "^0.2.0",
"@solana/web3.js": "^1.42.0",
"@switchboard-xyz/sbv2-utils": "^0.1.32",
"@switchboard-xyz/switchboard-v2": "^0.0.120",
"@switchboard-xyz/sbv2-utils": "^0.1.33",
"@switchboard-xyz/switchboard-v2": "^0.0.121",
"chalk": "^4.1.2",
"child_process": "^1.0.2",
"dotenv": "^16.0.1",

View File

@ -66,7 +66,10 @@ describe("anchor-vrf-parser test", () => {
before(async () => {
// First, attempt to load the switchboard devnet PID
try {
switchboard = await SwitchboardTestContext.loadDevnetQueue(provider);
switchboard = await SwitchboardTestContext.loadDevnetQueue(
provider,
"9e8rQPoyVZeGA1mD2BBVqDthbFwircnfLTB5JE6yUFQR"
);
console.log("devnet detected");
return;
} catch (error: any) {
@ -98,7 +101,7 @@ describe("anchor-vrf-parser test", () => {
authority: vrfClientKey, // vrf authority
keypair: vrfSecret,
});
const { escrow } = await vrfAccount.loadData();
console.log(`Created VRF Account: ${vrfAccount.publicKey}`);
const permissionAccount = await PermissionAccount.create(
@ -164,9 +167,10 @@ describe("anchor-vrf-parser test", () => {
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
const { escrow } = await vrfAccount.loadData();
// Request randomness
console.log(`Sending RequestRandomness instruction`);
const requestTxn = vrfClientProgram.methods.requestResult!({
vrfClientProgram.methods.requestResult!({
switchboardStateBump: programStateBump,
permissionBump,
})
@ -187,7 +191,10 @@ describe("anchor-vrf-parser test", () => {
tokenProgram: spl.TOKEN_PROGRAM_ID,
})
.signers([payer, payer])
.rpc();
.rpc()
.then((sig) => {
console.log(`RequestRandomness Txn: ${sig}`);
});
const result = await awaitCallback(
vrfClientProgram.provider.connection,

View File

@ -12,6 +12,6 @@ no-entrypoint = []
[dependencies]
switchboard-v2 = { path = "../../libraries/rs" }
# switchboard-v2 = "^0.1.10"
# switchboard-v2 = "^0.1.11"
solana-program = "1.9.29"

View File

@ -16,8 +16,8 @@
"dependencies": {
"@project-serum/anchor": "^0.24.2",
"@solana/web3.js": "^1.42.0",
"@switchboard-xyz/sbv2-utils": "^0.1.32",
"@switchboard-xyz/switchboard-v2": "^0.0.120"
"@switchboard-xyz/sbv2-utils": "^0.1.33",
"@switchboard-xyz/switchboard-v2": "^0.0.121"
},
"devDependencies": {
"@types/chai": "^4.3.0",

View File

@ -40,7 +40,13 @@ const anchorFeedKeypairPath = path.join(
"anchor_feed_parser-keypair.json"
);
const splFeedKeypairPath = path.join(
const anchorBufferKeypairPath = path.join(
targetDir,
"deploy",
"anchor_buffer_parser-keypair.json"
);
const nativeFeedKeypairPath = path.join(
targetDir,
"deploy",
"native_feed_parser-keypair.json"
@ -80,8 +86,11 @@ async function main() {
const anchorFeedParserPid = web3.Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync(anchorFeedKeypairPath, "utf8")))
).publicKey;
const splFeedParserPid = web3.Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync(splFeedKeypairPath, "utf8")))
const anchorBufferParserPid = web3.Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync(anchorBufferKeypairPath, "utf8")))
).publicKey;
const nativeFeedParserPid = web3.Keypair.fromSecretKey(
new Uint8Array(JSON.parse(fs.readFileSync(nativeFeedKeypairPath, "utf8")))
).publicKey;
// REPLACE ANCHOR-VRF-PROGRAM IDS
@ -113,17 +122,31 @@ async function main() {
anchorToml
);
console.log(`SPL Feed Parser PID: ${splFeedParserPid}`);
console.log(`Anchor Buffer Parser PID: ${anchorBufferParserPid}`);
shell.sed(
"-i",
/declare_id!(.*);/,
`declare_id!("${splFeedParserPid.toString()}");`,
`declare_id!("${anchorBufferParserPid.toString()}");`,
path.join(projectRoot, "programs", "anchor-buffer-parser", "src", "lib.rs")
);
shell.sed(
"-i",
/anchor_buffer_parser = "(.*)"/,
`anchor_buffer_parser = "${anchorBufferParserPid.toString()}"`,
anchorToml
);
console.log(`Native Feed Parser PID: ${nativeFeedParserPid}`);
shell.sed(
"-i",
/declare_id!(.*);/,
`declare_id!("${nativeFeedParserPid.toString()}");`,
path.join(projectRoot, "programs", "native-feed-parser", "src", "lib.rs")
);
shell.sed(
"-i",
/native_feed_parser = "(.*)"/,
`native_feed_parser = "${splFeedParserPid.toString()}"`,
`native_feed_parser = "${nativeFeedParserPid.toString()}"`,
anchorToml
);
@ -142,6 +165,7 @@ async function main() {
"anchor_vrf_parser.json"
)} ${vrfClientPath} --program-id ${anchorVrfParserPid.toString()}`
);
const feedClientPath = path.join(
projectRoot,
"programs",
@ -156,6 +180,21 @@ async function main() {
"anchor_feed_parser.json"
)} ${feedClientPath} --program-id ${anchorFeedParserPid.toString()}`
);
const bufferClientPath = path.join(
projectRoot,
"programs",
"anchor-buffer-parser",
"client"
);
shell.rm("-rf", bufferClientPath);
fs.mkdirSync(bufferClientPath, { recursive: true });
execSync(
`node ${anchorClientGen} ${path.join(
idlDir,
"anchor_buffer_parser.json"
)} ${bufferClientPath} --program-id ${anchorBufferParserPid.toString()}`
);
}
main()

108
yarn.lock
View File

@ -3942,6 +3942,29 @@
superstruct "^0.14.2"
tweetnacl "^1.0.0"
"@solana/web3.js@^1.47.3":
version "1.48.0"
resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.48.0.tgz#331281b2d80640431fb3b6fdc6b704ec325917aa"
integrity sha512-Gb6XvdhGjGI7CdAXLmlMIEvEYvrwqc78JOtwCsSrTqzz7Ek/BhJpZ/Cv89gxRDrWxf6kHegAfaN2FxwuYMmDZQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@ethersproject/sha2" "^5.5.0"
"@solana/buffer-layout" "^4.0.0"
bigint-buffer "^1.1.5"
bn.js "^5.0.0"
borsh "^0.7.0"
bs58 "^4.0.1"
buffer "6.0.1"
fast-stable-stringify "^1.0.0"
jayson "^3.4.4"
js-sha3 "^0.8.0"
node-fetch "2"
react-native-url-polyfill "^1.3.0"
rpc-websockets "^7.5.0"
secp256k1 "^4.0.2"
superstruct "^0.14.2"
tweetnacl "^1.0.0"
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906"
@ -4149,27 +4172,6 @@
"@svgr/plugin-jsx" "^6.2.1"
"@svgr/plugin-svgo" "^6.2.0"
"@switchboard-xyz/switchboard-v2@^0.0.115":
version "0.0.115"
resolved "https://registry.npmjs.org/@switchboard-xyz/switchboard-v2/-/switchboard-v2-0.0.115.tgz#9450357d8341b0432975363716ce44954408d83a"
integrity sha512-eM9CGs1HaKpExPtb56IQG0+j+Oy6nXM+5g4U7JK6DOR3SwbevCHoTjZt8EIgMuWr1M5NnGK9c0aRMygow+lFIg==
dependencies:
"@project-serum/anchor" "^0.24.2"
"@solana/spl-governance" "^0.0.34"
"@solana/spl-token" "^0.2.0"
"@solana/web3.js" "^1.44.3"
"@types/big.js" "^6.1.4"
assert "^2.0.0"
big.js "^6.2.0"
bs58 "^4.0.1"
chan "^0.6.1"
crypto-js "^4.0.0"
glob "^8.0.3"
long "^4.0.0"
mocha "^9.1.1"
node-fetch "^3.2.3"
protobufjs "^6.11.3"
"@switchboard-xyz/switchboard-v2@^0.0.120":
version "0.0.120"
resolved "https://registry.npmjs.org/@switchboard-xyz/switchboard-v2/-/switchboard-v2-0.0.120.tgz#b48d2292489771535dc4e9c6b5241115096291ec"
@ -4450,6 +4452,14 @@
resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@types/node-fetch@^2.6.2":
version "2.6.2"
resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "18.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a"
@ -6546,7 +6556,7 @@ combine-promises@^1.1.0:
resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.1.0.tgz#72db90743c0ca7aab7d0d8d2052fd7b0f674de71"
integrity sha512-ZI9jvcLDxqwaXEixOhArm3r7ReIivsXkpbyEWyeOhzz1QS0iSgBPnWvEqvIQtYyamGCYA88gFhmUrs9hrrQ0pg==
combined-stream@^1.0.6, combined-stream@~1.0.6:
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
@ -8931,6 +8941,15 @@ fork-ts-checker-webpack-plugin@^6.5.0:
semver "^7.3.2"
tapable "^1.0.0"
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
@ -12068,22 +12087,13 @@ node-emoji@^1.10.0:
dependencies:
lodash "^4.17.21"
node-fetch@2, node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7:
node-fetch@2, node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6, node-fetch@^2.6.1, node-fetch@^2.6.6, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.2.3:
version "3.2.8"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.8.tgz#0d9516dcf43a758d78d6dbe538adf0b1f6a4944e"
integrity sha512-KtpD1YhGszhntMpBDyp5lyagk8KIMopC1LEb7cQUAh7zcosaX5uK8HnbNb2i3NTQK3sIawCItS0uFC3QzcLHdg==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-fetch@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.6.tgz#6d4627181697a9d9674aae0d61548e0d629b31b9"
@ -13964,6 +13974,13 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
react-native-url-polyfill@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz#c1763de0f2a8c22cc3e959b654c8790622b6ef6a"
integrity sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ==
dependencies:
whatwg-url-without-unicode "8.0.0-3"
react-player@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.10.1.tgz#f2ee3ec31393d7042f727737545414b951ffc7e4"
@ -14640,6 +14657,19 @@ rpc-websockets@^7.4.2:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
rpc-websockets@^7.5.0:
version "7.5.0"
resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748"
integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==
dependencies:
"@babel/runtime" "^7.17.2"
eventemitter3 "^4.0.7"
uuid "^8.3.2"
ws "^8.5.0"
optionalDependencies:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
rtl-detect@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.4.tgz#40ae0ea7302a150b96bc75af7d749607392ecac6"
@ -16700,6 +16730,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
webidl-conversions@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
@ -16833,6 +16868,15 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-url-without-unicode@8.0.0-3:
version "8.0.0-3"
resolved "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b"
integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==
dependencies:
buffer "^5.4.3"
punycode "^2.1.1"
webidl-conversions "^5.0.0"
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"