sbv2-solana/libraries/ts/src/sbv2.ts

4302 lines
124 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
import * as anchor from "@project-serum/anchor";
import * as spl from "@solana/spl-token";
import {
AccountInfo,
AccountMeta,
clusterApiUrl,
ConfirmOptions,
Connection,
Keypair,
PublicKey,
sendAndConfirmTransaction,
Signer,
SystemProgram,
SYSVAR_INSTRUCTIONS_PUBKEY,
SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
Transaction,
TransactionInstruction,
TransactionSignature,
} from "@solana/web3.js";
import assert from "assert";
import Big from "big.js";
import * as crypto from "crypto";
import { OracleJob } from "./protos";
/**
* Switchboard Devnet Program ID
* 2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG
*/
export const SBV2_DEVNET_PID = new PublicKey(
"2TfB33aLaneQb5TNVwyDz3jSZXS6jdW2ARw1Dgf84XCG"
);
/**
* Switchboard Mainnet Program ID
* SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f
*/
export const SBV2_MAINNET_PID = new PublicKey(
"SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
);
export const GOVERNANCE_PID = new PublicKey(
//"GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw"
"2iNnEMZuLk2TysefLvXtS6kyvCFC7CDUTLLeatVgRend"
);
/**
* Load the Switchboard Program ID for a given cluster
* @param cluster solana cluster to fetch program ID for
* @return Switchboard Program ID Public Key
*/
export function getSwitchboardPid(
cluster: "devnet" | "mainnet-beta"
): PublicKey {
switch (cluster) {
case "devnet":
return SBV2_DEVNET_PID;
case "mainnet-beta":
return SBV2_MAINNET_PID;
default:
throw new Error(`no Switchboard PID associated with cluster ${cluster}`);
}
}
/**
* Load the Switchboard Program for a given cluster
* @param cluster solana cluster to interact with
* @param connection optional Connection object to use for rpc request
* @param payerKeypair optional Keypair to use for onchain txns. If ommited, a dummy keypair will be used and onchain txns will fail
* @param confirmOptions optional confirmation options for rpc request
* @return Switchboard Program
*/
export async function loadSwitchboardProgram(
cluster: "devnet" | "mainnet-beta",
connection = new Connection(clusterApiUrl(cluster)),
payerKeypair?: Keypair,
confirmOptions: ConfirmOptions = {
commitment: "confirmed",
}
): Promise<anchor.Program> {
const DEFAULT_KEYPAIR = Keypair.fromSeed(new Uint8Array(32).fill(1));
const programId = getSwitchboardPid(cluster);
const wallet: AnchorWallet = payerKeypair
? new AnchorWallet(payerKeypair)
: new AnchorWallet(DEFAULT_KEYPAIR);
const provider = new anchor.AnchorProvider(
connection,
wallet,
confirmOptions
);
const anchorIdl = await anchor.Program.fetchIdl(programId, provider);
if (!anchorIdl) {
throw new Error(`failed to read idl for ${cluster} ${programId}`);
}
return new anchor.Program(anchorIdl, programId, provider);
}
/**
* Switchboard precisioned representation of numbers.
*/
export class SwitchboardDecimal {
public constructor(
public readonly mantissa: anchor.BN,
public readonly scale: number
) {}
/**
* Convert untyped object to a Switchboard decimal, if possible.
* @param obj raw object to convert from
* @return SwitchboardDecimal
*/
public static from(obj: any): SwitchboardDecimal {
return new SwitchboardDecimal(new anchor.BN(obj.mantissa), obj.scale);
}
/**
* Convert a Big.js decimal to a Switchboard decimal.
* @param big a Big.js decimal
* @return a SwitchboardDecimal
*/
public static fromBig(big: Big): SwitchboardDecimal {
// Round to fit in Switchboard Decimal
// TODO: smarter logic.
big = big.round(20);
let mantissa: anchor.BN = new anchor.BN(big.c.join(""), 10);
// Set the scale. Big.exponenet sets scale from the opposite side
// SwitchboardDecimal does.
let scale = big.c.slice(1).length - big.e;
if (scale < 0) {
mantissa = mantissa.mul(
new anchor.BN(10, 10).pow(new anchor.BN(Math.abs(scale), 10))
);
scale = 0;
}
if (scale < 0) {
throw new Error(`SwitchboardDecimal: Unexpected negative scale.`);
}
if (scale >= 28) {
throw new Error("SwitchboardDecimalExcessiveScaleError");
}
// Set sign for the coefficient (mantissa)
mantissa = mantissa.mul(new anchor.BN(big.s, 10));
const result = new SwitchboardDecimal(mantissa, scale);
if (big.sub(result.toBig()).abs().gt(new Big(0.00005))) {
throw new Error(
`SwitchboardDecimal: Converted decimal does not match original:\n` +
`out: ${result.toBig().toNumber()} vs in: ${big.toNumber()}\n` +
`-- result mantissa and scale: ${result.mantissa.toString()} ${result.scale.toString()}\n` +
`${result} ${result.toBig()}`
);
}
return result;
}
/**
* SwitchboardDecimal equality comparator.
* @param other object to compare to.
* @return true iff equal
*/
public eq(other: SwitchboardDecimal): boolean {
return this.mantissa.eq(other.mantissa) && this.scale === other.scale;
}
/**
* Convert SwitchboardDecimal to big.js Big type.
* @return Big representation
*/
public toBig(): Big {
let mantissa: anchor.BN = new anchor.BN(this.mantissa, 10);
let s = 1;
const c: Array<number> = [];
const ZERO = new anchor.BN(0, 10);
const TEN = new anchor.BN(10, 10);
if (mantissa.lt(ZERO)) {
s = -1;
mantissa = mantissa.abs();
}
while (mantissa.gt(ZERO)) {
c.unshift(mantissa.mod(TEN).toNumber());
mantissa = mantissa.div(TEN);
}
const e = c.length - this.scale - 1;
const result = new Big(0);
if (c.length === 0) {
return result;
}
result.s = s;
result.c = c;
result.e = e;
return result;
}
}
/**
* Input parameters for constructing wrapped representations of Switchboard accounts.
*/
export interface AccountParams {
/**
* program referencing the Switchboard program and IDL.
*/
program: anchor.Program;
/**
* Public key of the account being referenced. This will always be populated
* within the account wrapper.
*/
publicKey?: PublicKey;
/**
* Keypair of the account being referenced. This may not always be populated.
*/
keypair?: Keypair;
}
/**
* Input parameters initializing program state.
*/
export interface ProgramInitParams {
mint?: PublicKey;
daoMint?: PublicKey;
}
export interface ProgramConfigParams {
mint?: PublicKey;
daoMint?: PublicKey;
}
/**
* Input parameters for transferring from Switchboard token vault.
*/
export interface VaultTransferParams {
amount: anchor.BN;
}
/**
* Account type representing Switchboard global program state.
*/
export class ProgramStateAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* ProgramStateAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Constructs ProgramStateAccount from the static seed from which it was generated.
* @return ProgramStateAccount and PDA bump tuple.
*/
static fromSeed(program: anchor.Program): [ProgramStateAccount, number] {
const [statePubkey, stateBump] =
anchor.utils.publicKey.findProgramAddressSync(
[Buffer.from("STATE")],
program.programId
);
return [
new ProgramStateAccount({ program, publicKey: statePubkey }),
stateBump,
];
}
/**
* Load and parse ProgramStateAccount state based on the program IDL.
* @return ProgramStateAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const state: any = await this.program.account.sbState.fetch(this.publicKey);
state.ebuf = undefined;
return state;
}
/**
* Fetch the Switchboard token mint specified in the program state account.
* @return Switchboard token mint.
*/
async getTokenMint(): Promise<spl.Token> {
const payerKeypair = programWallet(this.program);
const state = await this.loadData();
const switchTokenMint = new spl.Token(
this.program.provider.connection,
state.tokenMint,
spl.TOKEN_PROGRAM_ID,
payerKeypair
);
return switchTokenMint;
}
/**
* @return account size of the global ProgramStateAccount.
*/
size(): number {
return this.program.account.sbState.size;
}
static async getOrCreate(
program: anchor.Program,
params: ProgramInitParams
): Promise<[ProgramStateAccount, number]> {
const [account, seed] = ProgramStateAccount.fromSeed(program);
try {
await account.loadData();
} catch (e) {
try {
await ProgramStateAccount.create(program, params);
} catch {}
}
return [account, seed];
}
/**
* Create and initialize the ProgramStateAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated ProgramStateAccount.
*/
static async create(
program: anchor.Program,
params: ProgramInitParams
): Promise<ProgramStateAccount> {
const payerKeypair = programWallet(program);
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(program);
const psa = new ProgramStateAccount({
program,
publicKey: stateAccount.publicKey,
});
// Short circuit if already created.
try {
await psa.loadData();
return psa;
} catch (e) {}
let mint = null;
let vault = null;
if (params.mint === undefined) {
const decimals = 9;
const token = await spl.Token.createMint(
program.provider.connection,
payerKeypair,
payerKeypair.publicKey,
null,
decimals,
spl.TOKEN_PROGRAM_ID
);
const tokenVault = await token.createAccount(payerKeypair.publicKey);
mint = token.publicKey;
await token.mintTo(
tokenVault,
payerKeypair.publicKey,
[payerKeypair],
100_000_000
);
vault = tokenVault;
} else {
mint = params.mint;
const token = new spl.Token(
program.provider.connection,
mint,
spl.TOKEN_PROGRAM_ID,
payerKeypair
);
vault = await token.createAccount(payerKeypair.publicKey);
}
await program.methods
.programInit({
stateBump,
})
.accounts({
state: stateAccount.publicKey,
authority: payerKeypair.publicKey,
tokenMint: mint,
vault,
payer: payerKeypair.publicKey,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
daoMint: params.daoMint ?? mint,
})
.rpc();
return psa;
}
/**
* Transfer N tokens from the program vault to a specified account.
* @param to The recipient of the vault tokens.
* @param authority The vault authority required to sign the transfer tx.
* @param params specifies the amount to transfer.
* @return TransactionSignature
*/
async vaultTransfer(
to: PublicKey,
authority: Keypair,
params: VaultTransferParams
): Promise<TransactionSignature> {
const [statePubkey, stateBump] =
anchor.utils.publicKey.findProgramAddressSync(
[Buffer.from("STATE")],
this.program.programId
);
const vault = (await this.loadData()).tokenVault;
return this.program.methods
.vaultTransfer({
stateBump,
amount: params.amount,
})
.accounts({
state: statePubkey,
to,
vault,
authority: authority.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
})
.signers([authority])
.rpc();
}
}
/**
* Parameters to initialize an aggregator account.
*/
export interface AggregatorInitParams {
/**
* Name of the aggregator to store on-chain.
*/
name?: Buffer;
/**
* Metadata of the aggregator to store on-chain.
*/
metadata?: Buffer;
/**
* Number of oracles to request on aggregator update.
*/
batchSize: number;
/**
* Minimum number of oracle responses required before a round is validated.
*/
minRequiredOracleResults: number;
/**
* Minimum number of feed jobs suggested to be successful before an oracle
* sends a response.
*/
minRequiredJobResults: number;
/**
* Minimum number of seconds required between aggregator rounds.
*/
minUpdateDelaySeconds: number;
/**
* The queue to which this aggregator will be linked
*/
queueAccount: OracleQueueAccount;
/**
* unix_timestamp for which no feed update will occur before.
*/
startAfter?: number;
/**
* Change percentage required between a previous round and the current round.
* If variance percentage is not met, reject new oracle responses.
*/
varianceThreshold?: number;
/**
* Number of seconds for which, even if the variance threshold is not passed,
* accept new responses from oracles.
*/
forceReportPeriod?: anchor.BN;
/**
* unix_timestamp after which funds may be withdrawn from the aggregator.
* null/undefined/0 means the feed has no expiration.
*/
expiration?: anchor.BN;
/**
* Optional pre-existing keypair to use for aggregator initialization.
*/
keypair?: Keypair;
/**
* An optional wallet for receiving kickbacks from job usage in feeds.
* Defaults to token vault.
*/
authorWallet?: PublicKey;
/**
* If included, this keypair will be the aggregator authority rather than
* the aggregator keypair.
*/
authority?: PublicKey;
}
/**
* Parameters for which oracles must submit for responding to update requests.
*/
export interface AggregatorSaveResultParams {
/**
* Index in the list of oracles in the aggregator assigned to this round update.
*/
oracleIdx: number;
/**
* Reports that an error occured and the oracle could not send a value.
*/
error: boolean;
/**
* Value the oracle is responding with for this update.
*/
value: Big;
/**
* The minimum value this oracle has seen this round for the jobs listed in the
* aggregator.
*/
minResponse: Big;
/**
* The maximum value this oracle has seen this round for the jobs listed in the
* aggregator.
*/
maxResponse: Big;
/**
* List of OracleJobs that were performed to produce this result.
*/
jobs: Array<OracleJob>;
/**
* Authority of the queue the aggregator is attached to.
*/
queueAuthority: PublicKey;
/**
* Program token mint.
*/
tokenMint: PublicKey;
/**
* List of parsed oracles.
*/
oracles: Array<any>;
}
/**
* Parameters for creating and setting a history buffer for an aggregator
*/
export interface AggregatorSetHistoryBufferParams {
/*
* Authority keypair for the aggregator.
*/
authority?: Keypair;
/*
* Number of elements for the history buffer to fit.
*/
size: number;
}
/**
* Parameters required to open an aggregator round
*/
export interface AggregatorOpenRoundParams {
/**
* The oracle queue from which oracles are assigned this update.
*/
oracleQueueAccount: OracleQueueAccount;
/**
* The token wallet which will receive rewards for calling update on this feed.
*/
payoutWallet: PublicKey;
}
/**
* Switchboard wrapper for anchor program errors.
*/
export class SwitchboardError {
/**
* The program containing the Switchboard IDL specifying error codes.
*/
program: anchor.Program;
/**
* Stringified name of the error type.
*/
name: string;
/**
* Numerical SwitchboardError representation.
*/
code: number;
/**
* Message describing this error in detail.
*/
msg?: string;
/**
* Converts a numerical error code to a SwitchboardError based on the program
* IDL.
* @param program the Switchboard program object containing the program IDL.
* @param code Error code to convert to a SwitchboardError object.
* @return SwitchboardError
*/
static fromCode(program: anchor.Program, code: number): SwitchboardError {
for (const e of program.idl.errors ?? []) {
if (code === e.code) {
const r = new SwitchboardError();
r.program = program;
r.name = e.name;
r.code = e.code;
r.msg = e.msg;
return r;
}
}
throw new Error(`Could not find SwitchboardError for error code ${code}`);
}
}
/**
* Row structure of elements in the aggregator history buffer.
*/
export class AggregatorHistoryRow {
/**
* Timestamp of the aggregator result.
*/
timestamp: anchor.BN;
/**
* Aggregator value at timestamp.
*/
value: Big;
static from(buf: Buffer): AggregatorHistoryRow {
const timestamp = new anchor.BN(buf.slice(0, 8), "le");
// TODO(mgild): does this work for negative???
const mantissa = new anchor.BN(buf.slice(8, 24), "le");
const scale = buf.readUInt32LE(24);
const decimal = new SwitchboardDecimal(mantissa, scale);
const res = new AggregatorHistoryRow();
res.timestamp = timestamp;
res.value = decimal.toBig();
return res;
}
}
export interface AggregatorSetBatchSizeParams {
batchSize: number;
authority?: Keypair;
}
export interface AggregatorSetMinJobsParams {
minJobResults: number;
authority?: Keypair;
}
export interface AggregatorSetMinOraclesParams {
minOracleResults: number;
authority?: Keypair;
}
export interface AggregatorSetQueueParams {
queueAccount: OracleQueueAccount;
authority?: Keypair;
}
export interface AggregatorSetUpdateIntervalParams {
newInterval: number;
authority?: Keypair;
}
/**
* Account type representing an aggregator (data feed).
*/
export class AggregatorAccount {
program: anchor.Program;
publicKey?: PublicKey;
keypair?: Keypair;
/**
* AggregatorAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
static decode(
program: anchor.Program,
accountInfo: AccountInfo<Buffer>
): any {
const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "AggregatorAccountData";
const aggregator = coder.decode(key, accountInfo?.data!);
return aggregator;
}
/**
* Returns the aggregator's ID buffer in a stringified format.
* @param aggregator A preloaded aggregator object.
* @return The name of the aggregator.
*/
static getName(aggregator: any): string {
// eslint-disable-next-line no-control-regex
return String.fromCharCode(...aggregator.name).replace(/\u0000/g, "");
}
/**
* Load and parse AggregatorAccount state based on the program IDL.
* @return AggregatorAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const aggregator: any =
await this.program.account.aggregatorAccountData.fetch(this.publicKey);
aggregator.ebuf = undefined;
return aggregator;
}
async loadHistory(aggregator?: any): Promise<Array<AggregatorHistoryRow>> {
aggregator = aggregator ?? (await this.loadData());
if (aggregator.historyBuffer == PublicKey.default) {
return [];
}
const ROW_SIZE = 28;
let buffer =
(
await this.program.provider.connection.getAccountInfo(
aggregator.historyBuffer
)
)?.data ?? Buffer.from("");
if (buffer.length < 12) {
return [];
}
const insertIdx = buffer.readUInt32LE(8) * ROW_SIZE;
// console.log(insertIdx);
buffer = buffer.slice(12);
const front = [];
const tail = [];
for (let i = 0; i < buffer.length; i += ROW_SIZE) {
if (i + ROW_SIZE > buffer.length) {
break;
}
const row = AggregatorHistoryRow.from(buffer.slice(i, i + ROW_SIZE));
if (row.timestamp.eq(new anchor.BN(0))) {
break;
}
if (i <= insertIdx) {
tail.push(row);
} else {
front.push(row);
}
}
return front.concat(tail);
}
/**
* Get the latest confirmed value stored in the aggregator account.
* @param aggregator Optional parameter representing the already loaded
* aggregator info.
* @return latest feed value
*/
async getLatestValue(aggregator?: any, decimals = 20): Promise<Big | null> {
aggregator = aggregator ?? (await this.loadData());
if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) {
return null;
}
const mantissa = new Big(
aggregator.latestConfirmedRound.result.mantissa.toString()
);
const scale = aggregator.latestConfirmedRound.result.scale;
const oldDp = Big.DP;
Big.DP = decimals;
const result: Big = mantissa.div(new Big(10).pow(scale));
Big.DP = oldDp;
return result;
}
/**
* Get the timestamp latest confirmed round stored in the aggregator account.
* @param aggregator Optional parameter representing the already loaded
* aggregator info.
* @return latest feed timestamp
*/
async getLatestFeedTimestamp(aggregator?: any): Promise<anchor.BN> {
aggregator = aggregator ?? (await this.loadData());
if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) {
throw new Error("Aggregator currently holds no value.");
}
return aggregator.latestConfirmedRound.roundOpenTimestamp;
}
/**
* Speciifies if the aggregator settings recommend reporting a new value
* @param value The value which we are evaluating
* @param aggregator The loaded aggegator schema
* @returns boolean
*/
static async shouldReportValue(
value: Big,
aggregator: any
): Promise<boolean> {
if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) {
return true;
}
const timestamp: anchor.BN = new anchor.BN(Math.round(Date.now() / 1000));
if (aggregator.startAfter.gt(timestamp)) {
return false;
}
const varianceThreshold: Big = SwitchboardDecimal.from(
aggregator.varianceThreshold
).toBig();
const latestResult: Big = SwitchboardDecimal.from(
aggregator.latestConfirmedRound.result
).toBig();
const forceReportPeriod: anchor.BN = aggregator.forceReportPeriod;
const lastTimestamp: anchor.BN =
aggregator.latestConfirmedRound.roundOpenTimestamp;
if (lastTimestamp.add(aggregator.forceReportPeriod).lt(timestamp)) {
return true;
}
let diff = safeDiv(latestResult, value);
if (diff.abs().gt(1)) {
diff = safeDiv(value, latestResult);
}
// I dont want to think about variance percentage when values cross 0.
// Changes the scale of what we consider a "percentage".
if (diff.lt(0)) {
return true;
}
const changePercent = new Big(1).minus(diff).mul(100);
return changePercent.gt(varianceThreshold);
}
/**
* Get the individual oracle results of the latest confirmed round.
* @param aggregator Optional parameter representing the already loaded
* aggregator info.
* @return latest results by oracle pubkey
*/
async getConfirmedRoundResults(
aggregator?: any
): Promise<Array<{ oracleAccount: OracleAccount; value: Big }>> {
aggregator = aggregator ?? (await this.loadData());
if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) {
throw new Error("Aggregator currently holds no value.");
}
const results: Array<{ oracleAccount: OracleAccount; value: Big }> = [];
for (let i = 0; i < aggregator.oracleRequestBatchSize; ++i) {
if (aggregator.latestConfirmedRound.mediansFulfilled[i] === true) {
results.push({
oracleAccount: new OracleAccount({
program: this.program,
publicKey: aggregator.latestConfirmedRound.oraclePubkeysData[i],
}),
value: SwitchboardDecimal.from(
aggregator.latestConfirmedRound.mediansData[i]
).toBig(),
});
}
}
return results;
}
/**
* Produces a hash of all the jobs currently in the aggregator
* @return hash of all the feed jobs.
*/
produceJobsHash(jobs: Array<OracleJob>): crypto.Hash {
const hash = crypto.createHash("sha256");
for (const job of jobs) {
const jobHasher = crypto.createHash("sha256");
jobHasher.update(OracleJob.encodeDelimited(job).finish());
hash.update(jobHasher.digest());
}
return hash;
}
async loadCurrentRoundOracles(aggregator?: any): Promise<Array<any>> {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
aggregator = aggregator ?? (await this.loadData());
const oracleAccountDatas = await anchor.utils.rpc.getMultipleAccounts(
this.program.provider.connection,
aggregator.currentRound?.oraclePubkeysData?.slice(
0,
aggregator.oracleRequestBatchSize
)
);
if (oracleAccountDatas === null) {
throw new Error("Failed to load aggregator oracles");
}
return oracleAccountDatas.map((item) =>
coder.decode("OracleAccountData", item.account.data)
);
}
async loadJobAccounts(aggregator?: any): Promise<Array<any>> {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
aggregator = aggregator ?? (await this.loadData());
const jobAccountDatas = await anchor.utils.rpc.getMultipleAccounts(
this.program.provider.connection,
aggregator.jobPubkeysData.slice(0, aggregator.jobPubkeysSize)
);
if (jobAccountDatas === null) {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
return coder.decode("JobAccountData", item.account.data);
});
return jobs;
}
/**
* Load and deserialize all jobs stored in this aggregator
* @return Array<OracleJob>
*/
async loadJobs(aggregator?: any): Promise<Array<OracleJob>> {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
aggregator = aggregator ?? (await this.loadData());
const jobAccountDatas = await anchor.utils.rpc.getMultipleAccounts(
this.program.provider.connection,
aggregator.jobPubkeysData.slice(0, aggregator.jobPubkeysSize)
);
if (jobAccountDatas === null) {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
const decoded = coder.decode("JobAccountData", item.account.data);
return OracleJob.decodeDelimited(decoded.data);
});
return jobs;
}
async loadHashes(aggregator?: any): Promise<Array<Buffer>> {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
aggregator = aggregator ?? (await this.loadData());
const jobAccountDatas = await anchor.utils.rpc.getMultipleAccounts(
this.program.provider.connection,
aggregator.jobPubkeysData.slice(0, aggregator.jobPubkeysSize)
);
if (jobAccountDatas === null) {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
const decoded = coder.decode("JobAccountData", item.account.data);
return decoded.hash;
});
return jobs;
}
/**
* Get the size of an AggregatorAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.aggregatorAccountData.size;
}
/**
* Create and initialize the AggregatorAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated AggregatorAccount.
*/
static async create(
program: anchor.Program,
params: AggregatorInitParams
): Promise<AggregatorAccount> {
const payerKeypair = programWallet(program);
const aggregatorAccount = params.keypair ?? anchor.web3.Keypair.generate();
const authority = params.authority ?? aggregatorAccount.publicKey;
const size = program.account.aggregatorAccountData.size;
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(program);
const state = await stateAccount.loadData();
await program.methods
.aggregatorInit({
name: (params.name ?? Buffer.from("")).slice(0, 32),
metadata: (params.metadata ?? Buffer.from("")).slice(0, 128),
batchSize: params.batchSize,
minOracleResults: params.minRequiredOracleResults,
minJobResults: params.minRequiredJobResults,
minUpdateDelaySeconds: params.minUpdateDelaySeconds,
varianceThreshold: SwitchboardDecimal.fromBig(
new Big(params.varianceThreshold ?? 0)
),
forceReportPeriod: params.forceReportPeriod ?? new anchor.BN(0),
expiration: params.expiration ?? new anchor.BN(0),
stateBump,
})
.accounts({
aggregator: aggregatorAccount.publicKey,
authority,
queue: params.queueAccount.publicKey,
authorWallet: params.authorWallet ?? state.tokenVault,
programState: stateAccount.publicKey,
})
.signers([aggregatorAccount])
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: aggregatorAccount.publicKey,
space: size,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
size
),
programId: program.programId,
}),
])
.rpc();
return new AggregatorAccount({ program, keypair: aggregatorAccount });
}
async setBatchSize(
params: AggregatorSetBatchSizeParams
): Promise<TransactionSignature> {
const program = this.program;
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return program.methods
.aggregatorSetBatchSize({
batchSize: params.batchSize,
})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
async setVarianceThreshold(params: {
authority: Keypair;
threshold: Big;
}): Promise<TransactionSignature> {
const program = this.program;
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return program.rpc.aggregatorSetVarianceThreshold(
{
varianceThreshold: SwitchboardDecimal.fromBig(params.threshold),
},
{
accounts: {
aggregator: this.publicKey,
authority: authority.publicKey,
},
signers: [authority],
}
);
}
async setMinJobs(
params: AggregatorSetMinJobsParams
): Promise<TransactionSignature> {
const program = this.program;
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return program.methods
.aggregatorSetMinJobs({
minJobResults: params.minJobResults,
})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
async setMinOracles(
params: AggregatorSetMinOraclesParams
): Promise<TransactionSignature> {
const program = this.program;
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return program.methods
.aggregatorSetMinOracles({
minOracleResults: params.minOracleResults,
})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
async setHistoryBuffer(
params: AggregatorSetHistoryBufferParams
): Promise<TransactionSignature> {
const buffer = Keypair.generate();
const program = this.program;
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
const HISTORY_ROW_SIZE = 28;
const INSERT_IDX_SIZE = 4;
const DISCRIMINATOR_SIZE = 8;
const size =
params.size * HISTORY_ROW_SIZE + INSERT_IDX_SIZE + DISCRIMINATOR_SIZE;
return program.methods
.aggregatorSetHistoryBuffer({})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
buffer: buffer.publicKey,
})
.signers([authority, buffer])
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: buffer.publicKey,
space: size,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
size
),
programId: program.programId,
}),
])
.rpc();
}
async setUpdateInterval(
params: AggregatorSetUpdateIntervalParams
): Promise<TransactionSignature> {
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorSetUpdateInterval({
newInterval: params.newInterval,
})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
async setQueue(
params: AggregatorSetQueueParams
): Promise<TransactionSignature> {
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorSetQueue({})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
queue: params.queueAccount.publicKey,
})
.signers([authority])
.rpc();
}
/**
* RPC to add a new job to an aggregtor to be performed on feed updates.
* @param job JobAccount specifying another job for this aggregator to fulfill on update
* @return TransactionSignature
*/
async addJob(
job: JobAccount,
authority?: Keypair,
weight = 1
): Promise<TransactionSignature> {
authority = authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorAddJob({
weight,
})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
job: job.publicKey,
})
.signers([authority])
.rpc();
}
/**
* Prevent new jobs from being added to the feed.
* @param authority The current authroity keypair
* @return TransactionSignature
*/
async lock(authority?: Keypair): Promise<TransactionSignature> {
authority = authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorLock({})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
/**
* Change the aggregator authority.
* @param currentAuthority The current authroity keypair
* @param newAuthority The new authority to set.
* @return TransactionSignature
*/
async setAuthority(
newAuthority: PublicKey,
currentAuthority?: Keypair
): Promise<TransactionSignature> {
currentAuthority =
currentAuthority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorSetAuthority({})
.accounts({
aggregator: this.publicKey,
newAuthority,
authority: currentAuthority.publicKey,
})
.signers([currentAuthority])
.rpc();
}
/**
* RPC to remove a job from an aggregtor.
* @param job JobAccount to be removed from the aggregator
* @return TransactionSignature
*/
async removeJob(
job: JobAccount,
authority?: Keypair
): Promise<TransactionSignature> {
authority = authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.aggregatorRemoveJob({})
.accounts({
aggregator: this.publicKey,
authority: authority.publicKey,
job: job.publicKey,
})
.signers([authority])
.rpc();
}
/**
* Opens a new round for the aggregator and will provide an incentivize reward
* to the caller
* @param params
* @return TransactionSignature
*/
async openRound(
params: AggregatorOpenRoundParams
): Promise<TransactionSignature> {
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
this.program,
params.oracleQueueAccount,
this
);
try {
await leaseAccount.loadData();
} catch (_) {
throw new Error(
"A requested lease pda account has not been initialized."
);
}
const escrowPubkey = (await leaseAccount.loadData()).escrow;
const queue = await params.oracleQueueAccount.loadData();
const queueAuthority = queue.authority;
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
params.oracleQueueAccount.publicKey,
this.publicKey
);
try {
await permissionAccount.loadData();
} catch (_) {
throw new Error(
"A requested permission pda account has not been initialized."
);
}
return this.program.methods
.aggregatorOpenRound({
stateBump,
leaseBump,
permissionBump,
})
.accounts({
aggregator: this.publicKey,
lease: leaseAccount.publicKey,
oracleQueue: params.oracleQueueAccount.publicKey,
queueAuthority,
permission: permissionAccount.publicKey,
escrow: escrowPubkey,
programState: stateAccount.publicKey,
payoutWallet: params.payoutWallet,
tokenProgram: spl.TOKEN_PROGRAM_ID,
dataBuffer: queue.dataBuffer,
mint: (await params.oracleQueueAccount.loadMint()).publicKey,
})
.rpc();
}
async getOracleIndex(oraclePubkey: PublicKey): Promise<number> {
const aggregator = await this.loadData();
for (let i = 0; i < aggregator.oracleRequestBatchSize; i++) {
if (aggregator.currentRound.oraclePubkeysData[i].equals(oraclePubkey)) {
return i;
}
}
return -1;
}
async saveResult(
aggregator: any,
oracleAccount: OracleAccount,
params: AggregatorSaveResultParams
): Promise<TransactionSignature> {
return (
await this.program.provider.sendAll([
{
tx: await this.saveResultTxn(aggregator, oracleAccount, params),
signers: [programWallet(this.program)],
},
])
)[0];
}
/**
* RPC for an oracle to save a result to an aggregator round.
* @param oracleAccount The oracle account submitting a result.
* @param params
* @return TransactionSignature
*/
async saveResultTxn(
aggregator: any,
oracleAccount: OracleAccount, // TODO: move to params.
params: AggregatorSaveResultParams
): Promise<Transaction> {
let oracles = params.oracles ?? [];
if (oracles.length === 0) {
oracles = await this.loadCurrentRoundOracles(aggregator);
}
const payerKeypair = programWallet(this.program);
const remainingAccounts: Array<PublicKey> = [];
for (let i = 0; i < aggregator.oracleRequestBatchSize; ++i) {
remainingAccounts.push(aggregator.currentRound.oraclePubkeysData[i]);
}
for (const oracle of oracles) {
remainingAccounts.push(oracle.tokenAccount);
}
const queuePubkey = aggregator.queuePubkey;
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: queuePubkey,
});
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
this.program,
queueAccount,
this
);
// const escrow = await spl.Token.getAssociatedTokenAddress(
// spl.ASSOCIATED_TOKEN_PROGRAM_ID,
// params.tokenMint,
// this.program.programId,
// leaseAccount.publicKey
// );
const escrow = await spl.Token.getAssociatedTokenAddress(
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
spl.TOKEN_PROGRAM_ID,
params.tokenMint,
leaseAccount.publicKey,
true
);
const [feedPermissionAccount, feedPermissionBump] =
PermissionAccount.fromSeed(
this.program,
params.queueAuthority,
queueAccount.publicKey,
this.publicKey
);
const [oraclePermissionAccount, oraclePermissionBump] =
PermissionAccount.fromSeed(
this.program,
params.queueAuthority,
queueAccount.publicKey,
oracleAccount.publicKey
);
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const digest = this.produceJobsHash(params.jobs).digest();
let historyBuffer = aggregator.historyBuffer;
if (historyBuffer.equals(PublicKey.default)) {
historyBuffer = this.publicKey;
}
return this.program.methods
.aggregatorSaveResult({
oracleIdx: params.oracleIdx,
error: params.error,
value: SwitchboardDecimal.fromBig(params.value),
jobsChecksum: digest,
minResponse: SwitchboardDecimal.fromBig(params.minResponse),
maxResponse: SwitchboardDecimal.fromBig(params.maxResponse),
feedPermissionBump,
oraclePermissionBump,
leaseBump,
stateBump,
})
.accounts({
aggregator: this.publicKey,
oracle: oracleAccount.publicKey,
oracleAuthority: payerKeypair.publicKey,
oracleQueue: queueAccount.publicKey,
queueAuthority: params.queueAuthority,
feedPermission: feedPermissionAccount.publicKey,
oraclePermission: oraclePermissionAccount.publicKey,
lease: leaseAccount.publicKey,
escrow,
tokenProgram: spl.TOKEN_PROGRAM_ID,
programState: programStateAccount.publicKey,
historyBuffer,
mint: params.tokenMint,
})
.remainingAccounts(
remainingAccounts.map((pubkey: PublicKey) => {
return { isSigner: false, isWritable: true, pubkey };
})
)
.transaction();
}
}
/**
* Parameters for initializing JobAccount
*/
export interface JobInitParams {
/**
* An optional name to apply to the job account.
*/
name?: Buffer;
/**
* unix_timestamp of when funds can be withdrawn from this account.
*/
expiration?: anchor.BN;
/**
* A serialized protocol buffer holding the schema of the job.
*/
data: Buffer;
/**
* A required variables oracles must fill to complete the job.
*/
variables?: Array<string>;
/**
* A pre-generated keypair to use.
*/
keypair?: Keypair;
authority: PublicKey;
}
/**
* A Switchboard account representing a job for an oracle to perform, stored as
* a protocol buffer.
*/
export class JobAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* JobAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Load and parse JobAccount data based on the program IDL.
* @return JobAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const job = await this.program.account.jobAccountData.fetch(this.publicKey);
return job;
}
/**
* Load and parse the protobuf from the raw buffer stored in the JobAccount.
* @return OracleJob
*/
async loadJob(): Promise<OracleJob> {
const job = await this.loadData();
return OracleJob.decodeDelimited(job.data);
}
/**
* Create and initialize the JobAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated JobAccount.
*/
static async create(
program: anchor.Program,
params: JobInitParams
): Promise<JobAccount> {
const payerKeypair = programWallet(program);
const jobAccount = params.keypair ?? anchor.web3.Keypair.generate();
const size =
280 + params.data.length + (params.variables?.join("")?.length ?? 0);
const [stateAccount, stateBump] = await ProgramStateAccount.getOrCreate(
program,
{}
);
const state = await stateAccount.loadData();
await program.methods
.jobInit({
name: params.name ?? Buffer.from(""),
expiration: params.expiration ?? new anchor.BN(0),
data: params.data,
variables:
params.variables?.map((item) => Buffer.from("")) ??
new Array<Buffer>(),
stateBump,
})
.accounts({
job: jobAccount.publicKey,
authorWallet: params.authority,
authority: params.authority,
programState: stateAccount.publicKey,
})
.signers([jobAccount])
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: jobAccount.publicKey,
space: size,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
size
),
programId: program.programId,
}),
])
.rpc();
return new JobAccount({ program, keypair: jobAccount });
}
static decode(
program: anchor.Program,
accountInfo: AccountInfo<Buffer>
): any {
const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "JobAccountData";
const data = coder.decode(key, accountInfo?.data!);
return data;
}
static decodeJob(
program: anchor.Program,
accountInfo: AccountInfo<Buffer>
): OracleJob {
return OracleJob.decodeDelimited(
JobAccount.decode(program, accountInfo).data!
);
}
}
/**
* Parameters for initializing PermissionAccount
*/
export interface PermissionInitParams {
/**
* Pubkey of the account granting the permission.
*/
granter: PublicKey;
/**
* The receiving account of a permission.
*/
grantee: PublicKey;
/**
* The authority that is allowed to set permissions for this account.
*/
authority: PublicKey;
}
/**
* Parameters for setting a permission in a PermissionAccount
*/
export interface PermissionSetParams {
/**
* The permssion to set
*/
permission: SwitchboardPermission;
/**
* The authority controlling this permission.
*/
//authority: Keypair | PublicKey;
authority: Keypair | PublicKey;
/**
* Specifies whether to enable or disable the permission.
*/
enable: boolean;
}
export interface PermissionSetVoterWeightParams {
govProgram: PublicKey;
pubkeySigner?: PublicKey;
addinProgram: anchor.Program;
realm: PublicKey;
}
/**
* An enum representing all known permission types for Switchboard.
*/
export enum SwitchboardPermission {
PERMIT_ORACLE_HEARTBEAT = "permitOracleHeartbeat",
PERMIT_ORACLE_QUEUE_USAGE = "permitOracleQueueUsage",
PERMIT_VRF_REQUESTS = "permitVrfRequests",
}
export enum SwitchboardPermissionValue {
PERMIT_ORACLE_HEARTBEAT = 1 << 0,
PERMIT_ORACLE_QUEUE_USAGE = 1 << 1,
PERMIT_VRF_REQUESTS = 1 << 2,
}
/**
* A Switchboard account representing a permission or privilege granted by one
* account signer to another account.
*/
export class PermissionAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* PermissionAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Check if a specific permission is enabled on this permission account
*/
async isPermissionEnabled(
permission: SwitchboardPermissionValue
): Promise<boolean> {
const permissions = (await this.loadData()).permissions;
return (permissions & (permission as number)) != 0;
}
/**
* Load and parse PermissionAccount data based on the program IDL.
* @return PermissionAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const permission: any =
await this.program.account.permissionAccountData.fetch(this.publicKey);
permission.ebuf = undefined;
return permission;
}
/**
* Get the size of a PermissionAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.permissionAccountData.size;
}
/**
* Create and initialize the PermissionAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated PermissionAccount.
*/
static async create(
program: anchor.Program,
params: PermissionInitParams
): Promise<PermissionAccount> {
const authorityInfo = await program.provider.connection.getAccountInfo(
params.authority
);
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
program,
params.authority,
params.granter,
params.grantee
);
const payerKeypair = programWallet(program);
await program.methods
.permissionInit({})
.accounts({
permission: permissionAccount.publicKey,
authority: params.authority,
granter: params.granter,
grantee: params.grantee,
payer: programWallet(program).publicKey,
systemProgram: SystemProgram.programId,
})
.signers([payerKeypair])
.rpc();
return new PermissionAccount({
program,
publicKey: permissionAccount.publicKey,
});
}
/**
* Loads a PermissionAccount from the expected PDA seed format.
* @param authority The authority pubkey to be incorporated into the account seed.
* @param granter The granter pubkey to be incorporated into the account seed.
* @param grantee The grantee pubkey to be incorporated into the account seed.
* @return PermissionAccount and PDA bump.
*/
static fromSeed(
program: anchor.Program,
authority: PublicKey,
granter: PublicKey,
grantee: PublicKey
): [PermissionAccount, number] {
const [pubkey, bump] = anchor.utils.publicKey.findProgramAddressSync(
[
Buffer.from("PermissionAccountData"),
authority.toBytes(),
granter.toBytes(),
grantee.toBytes(),
],
program.programId
);
return [new PermissionAccount({ program, publicKey: pubkey }), bump];
}
/**
* Sets the permission in the PermissionAccount
* @param params.
* @return TransactionSignature.
*/
async set(params: PermissionSetParams): Promise<TransactionSignature> {
if (!("publicKey" in params.authority)) {
throw new Error(
"Authority cannot be a PublicKey for the set RPC method."
);
}
const permissionData = await this.loadData();
const authorityInfo = await this.program.provider.connection.getAccountInfo(
permissionData.authority
);
const permission = new Map<string, null>();
permission.set(params.permission.toString(), null);
return this.program.methods
.permissionSet({
permission: Object.fromEntries(permission),
enable: params.enable,
})
.accounts({
permission: this.publicKey,
authority: params.authority.publicKey,
})
.signers([params.authority])
.rpc();
}
/**
* Sets the permission in the PermissionAccount
* @param params.
* @return TransactionSignature.
*/
async setTx(params: PermissionSetParams): Promise<Transaction> {
const permissionData = await this.loadData();
let authPk: PublicKey;
const signers: Array<Keypair> = [];
if ("publicKey" in params.authority) {
authPk = params.authority.publicKey;
signers.push(params.authority as Keypair);
} else {
authPk = params.authority;
}
const authorityInfo = await this.program.provider.connection.getAccountInfo(
permissionData.authority
);
const permission = new Map<string, null>();
permission.set(params.permission.toString(), null);
console.log("authority:");
console.log(authPk);
return this.program.methods
.permissionSet({
permission: Object.fromEntries(permission),
enable: params.enable,
})
.accounts({
permission: this.publicKey,
authority: authPk,
})
.signers(signers)
.transaction();
}
async setVoterWeightTx(params: PermissionSetVoterWeightParams) {
const permissionData = await this.loadData();
const oracleData = await this.program.account.oracleAccountData.fetch(
permissionData.grantee
);
let payerKeypair;
if (params.pubkeySigner == undefined) {
payerKeypair = programWallet(this.program);
}
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const psData = await programStateAccount.loadData();
const [addinState, _] = await PublicKey.findProgramAddress(
[Buffer.from("state")],
params.addinProgram.programId
);
const [realmSpawnRecord] = anchor.utils.publicKey.findProgramAddressSync(
[Buffer.from("RealmSpawnRecord"), params.realm.toBytes()],
params.addinProgram.programId
);
const [voterWeightRecord] = anchor.utils.publicKey.findProgramAddressSync(
[Buffer.from("VoterWeightRecord"), permissionData.grantee.toBytes()],
params.addinProgram.programId
);
const [tokenOwnerRecord] = anchor.utils.publicKey.findProgramAddressSync(
[
Buffer.from("governance"),
params.realm.toBytes(),
psData.daoMint.toBytes(),
(oracleData.oracleAuthority as PublicKey).toBytes(),
],
params.govProgram
);
return params.addinProgram.methods
.permissionSetVoterWeight()
.accounts({
permission: this.publicKey,
permissionAuthority: permissionData.authority,
oracle: permissionData.grantee,
oracleAuthority: oracleData.oracleAuthority as PublicKey,
payer: params.pubkeySigner,
systemProgram: SystemProgram.programId,
sbState: programStateAccount.publicKey,
programState: addinState,
govProgram: GOVERNANCE_PID,
daoMint: psData.daoMint,
spawnRecord: realmSpawnRecord,
voterWeight: voterWeightRecord,
tokenOwnerRecord: tokenOwnerRecord,
realm: params.realm,
})
.transaction();
}
}
/**
* Parameters for initializing OracleQueueAccount
*/
export interface OracleQueueInitParams {
/**
* A name to assign to this OracleQueue
*/
name?: Buffer;
/**
* Buffer for queue metadata
*/
metadata?: Buffer;
/**
* Rewards to provide oracles and round openers on this queue.
*/
reward: anchor.BN;
/**
* The minimum amount of stake oracles must present to remain on the queue.
*/
minStake: anchor.BN;
/**
* After a feed lease is funded or re-funded, it must consecutively succeed
* N amount of times or its authorization to use the queue is auto-revoked.
*/
feedProbationPeriod?: number;
/**
* The account to delegate authority to for creating permissions targeted
* at the queue.
*/
authority: PublicKey;
/**
* Time period we should remove an oracle after if no response.
*/
oracleTimeout?: anchor.BN;
/**
* Whether slashing is enabled on this queue.
*/
slashingEnabled?: boolean;
/**
* The tolerated variance amount oracle results can have from the
* accepted round result before being slashed.
* slashBound = varianceToleranceMultiplier * stdDeviation
* Default: 2
*/
varianceToleranceMultiplier?: number;
/**
* Consecutive failure limit for a feed before feed permission is revoked.
*/
consecutiveFeedFailureLimit?: anchor.BN;
/**
* TODO: implement
* Consecutive failure limit for an oracle before oracle permission is revoked.
*/
consecutiveOracleFailureLimit?: anchor.BN;
/**
* the minimum update delay time for Aggregators
*/
minimumDelaySeconds?: number;
/**
* Optionally set the size of the queue.
*/
queueSize?: number;
/**
* Eanbling 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
* to request VRF proofs and verifications from this queue.
*/
unpermissionedVrf?: boolean;
mint: PublicKey;
}
export interface OracleQueueSetRewardsParams {
rewards: anchor.BN;
authority?: Keypair;
}
export interface OracleQueueSetVrfSettingsParams {
unpermissionedVrf: boolean;
authority?: Keypair;
}
/**
* A Switchboard account representing a queue for distributing oracles to
* permitted data feeds.
*/
export class OracleQueueAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* OracleQueueAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
async loadMint(): Promise<spl.Token> {
const payerKeypair = programWallet(this.program);
const queue = await this.loadData();
let mintKey = queue.mint ?? PublicKey.default;
if (mintKey.equals(PublicKey.default)) {
mintKey = spl.NATIVE_MINT;
}
return new spl.Token(
this.program.provider.connection,
mintKey,
spl.TOKEN_PROGRAM_ID,
payerKeypair
);
}
/**
* Load and parse OracleQueueAccount data based on the program IDL.
* @return OracleQueueAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const queue: any = await this.program.account.oracleQueueAccountData.fetch(
this.publicKey
);
const queueData = [];
const buffer =
(
await this.program.provider.connection.getAccountInfo(queue.dataBuffer)
)?.data.slice(8) ?? Buffer.from("");
const rowSize = 32;
for (let i = 0; i < queue.size * rowSize; i += rowSize) {
if (buffer.length - i < rowSize) {
break;
}
const pubkeyBuf = buffer.slice(i, i + rowSize);
const key = new PublicKey(pubkeyBuf);
if (key === PublicKey.default) {
break;
}
queueData.push(key);
}
queue.queue = queueData;
queue.ebuf = undefined;
return queue;
}
/**
* Get the size of an OracleQueueAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.oracleQueueAccountData.size;
}
/**
* Create and initialize the OracleQueueAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated OracleQueueAccount.
*/
static async create(
program: anchor.Program,
params: OracleQueueInitParams
): Promise<OracleQueueAccount> {
const payerKeypair = programWallet(program);
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(program);
/*const mint = (await stateAccount.getTokenMint()).publicKey;*/
const mint = params.mint;
const oracleQueueAccount = anchor.web3.Keypair.generate();
const buffer = anchor.web3.Keypair.generate();
const size = program.account.oracleQueueAccountData.size;
params.queueSize = params.queueSize ?? 500;
const queueSize = params.queueSize * 32 + 8;
await program.methods
.oracleQueueInit({
name: (params.name ?? Buffer.from("")).slice(0, 32),
metadata: (params.metadata ?? Buffer.from("")).slice(0, 64),
reward: params.reward ?? new anchor.BN(0),
minStake: params.minStake ?? new anchor.BN(0),
feedProbationPeriod: params.feedProbationPeriod ?? 0,
oracleTimeout: params.oracleTimeout ?? 180,
slashingEnabled: params.slashingEnabled ?? false,
varianceToleranceMultiplier: SwitchboardDecimal.fromBig(
new Big(params.varianceToleranceMultiplier ?? 2)
),
authority: params.authority,
consecutiveFeedFailureLimit:
params.consecutiveFeedFailureLimit ?? new anchor.BN(1000),
consecutiveOracleFailureLimit:
params.consecutiveOracleFailureLimit ?? new anchor.BN(1000),
minimumDelaySeconds: params.minimumDelaySeconds ?? 5,
queueSize: params.queueSize,
unpermissionedFeeds: params.unpermissionedFeeds ?? false,
})
.accounts({
oracleQueue: oracleQueueAccount.publicKey,
authority: params.authority,
buffer: buffer.publicKey,
systemProgram: SystemProgram.programId,
payer: programWallet(program).publicKey,
mint,
})
.signers([oracleQueueAccount, buffer])
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: buffer.publicKey,
space: queueSize,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
queueSize
),
programId: program.programId,
}),
])
.rpc();
return new OracleQueueAccount({ program, keypair: oracleQueueAccount });
}
async setRewards(
params: OracleQueueSetRewardsParams
): Promise<TransactionSignature> {
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.oracleQueueSetRewards({
rewards: params.rewards,
})
.accounts({ queue: this.publicKey, authority: authority.publicKey })
.signers([authority])
.rpc();
}
async setVrfSettings(
params: OracleQueueSetVrfSettingsParams
): Promise<TransactionSignature> {
const authority =
params.authority ?? this.keypair ?? programWallet(this.program);
return this.program.methods
.oracleQueueVrfConfig({
unpermissionedVrfEnabled: params.unpermissionedVrf,
})
.accounts({
queue: this.publicKey,
authority: authority.publicKey,
})
.signers([authority])
.rpc();
}
}
/**
* Parameters for initializing a LeaseAccount
*/
export interface LeaseInitParams {
/**
* Token amount to load into the lease escrow
*/
loadAmount: anchor.BN;
/**
* The funding wallet of the lease.
*/
funder: PublicKey;
/**
* The authority of the funding wallet
*/
funderAuthority: Keypair;
/**
* The target to which this lease is applied.
*/
oracleQueueAccount: OracleQueueAccount;
/**
* The feed which the lease grants permission.
*/
aggregatorAccount: AggregatorAccount;
/**
* This authority will be permitted to withdraw funds from this lease.
*/
withdrawAuthority?: PublicKey;
}
/**
* Parameters for extending a LeaseAccount
*/
export interface LeaseExtendParams {
/**
* Token amount to load into the lease escrow
*/
loadAmount: anchor.BN;
/**
* The funding wallet of the lease.
*/
funder: PublicKey;
/**
* The authority of the funding wallet
*/
funderAuthority: Keypair;
}
/**
* Parameters for withdrawing from a LeaseAccount
*/
export interface LeaseWithdrawParams {
/**
* Token amount to withdraw from the lease escrow
*/
amount: anchor.BN;
/**
* The wallet to withdraw to.
*/
withdrawWallet: PublicKey;
/**
* The withdraw authority of the lease
*/
withdrawAuthority: Keypair;
}
/**
* A Switchboard account representing a lease for managing funds for oracle payouts
* for fulfilling feed updates.
*/
export class LeaseAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* LeaseAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Loads a LeaseAccount from the expected PDA seed format.
* @param leaser The leaser pubkey to be incorporated into the account seed.
* @param target The target pubkey to be incorporated into the account seed.
* @return LeaseAccount and PDA bump.
*/
static fromSeed(
program: anchor.Program,
queueAccount: OracleQueueAccount,
aggregatorAccount: AggregatorAccount
): [LeaseAccount, number] {
const [pubkey, bump] = anchor.utils.publicKey.findProgramAddressSync(
[
Buffer.from("LeaseAccountData"),
queueAccount.publicKey.toBytes(),
aggregatorAccount.publicKey.toBytes(),
],
program.programId
);
return [new LeaseAccount({ program, publicKey: pubkey }), bump];
}
/**
* Load and parse LeaseAccount data based on the program IDL.
* @return LeaseAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const lease: any = await this.program.account.leaseAccountData.fetch(
this.publicKey
);
lease.ebuf = undefined;
return lease;
}
/**
* Get the size of a LeaseAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.leaseAccountData.size;
}
/**
* Create and initialize the LeaseAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated LeaseAccount.
*/
static async create(
program: anchor.Program,
params: LeaseInitParams
): Promise<LeaseAccount> {
const payerKeypair = programWallet(program);
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const switchTokenMint = await params.oracleQueueAccount.loadMint();
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
program,
params.oracleQueueAccount,
params.aggregatorAccount
);
const escrow = await spl.Token.getAssociatedTokenAddress(
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
spl.TOKEN_PROGRAM_ID,
switchTokenMint.publicKey,
leaseAccount.publicKey,
true
);
await (switchTokenMint as any).createAssociatedTokenAccountInternal(
leaseAccount.publicKey,
escrow
);
const jobAccountDatas = await params.aggregatorAccount.loadJobAccounts();
const aggregatorData = await params.aggregatorAccount.loadData();
const jobPubkeys = aggregatorData.jobPubkeysData.slice(
0,
aggregatorData.jobPubkeysSize
);
const jobWallets: Array<PublicKey> = [];
const walletBumps: Array<number> = [];
for (const idx in jobAccountDatas) {
const jobAccountData = jobAccountDatas[idx];
const authority = jobAccountData.authority ?? PublicKey.default;
const [jobWallet, bump] = await PublicKey.findProgramAddress(
[
authority.toBuffer(),
spl.TOKEN_PROGRAM_ID.toBuffer(),
switchTokenMint.publicKey.toBuffer(),
],
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
jobWallets.push(jobWallet);
walletBumps.push(bump);
}
await program.methods
.leaseInit({
loadAmount: params.loadAmount,
stateBump,
leaseBump,
withdrawAuthority: params.withdrawAuthority ?? PublicKey.default,
walletBumps: Buffer.from(walletBumps),
})
.accounts({
programState: programStateAccount.publicKey,
lease: leaseAccount.publicKey,
queue: params.oracleQueueAccount.publicKey,
aggregator: params.aggregatorAccount.publicKey,
systemProgram: SystemProgram.programId,
funder: params.funder,
payer: programWallet(program).publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
escrow,
owner: params.funderAuthority.publicKey,
mint: switchTokenMint.publicKey,
})
.signers([params.funderAuthority])
.remainingAccounts(
jobPubkeys.concat(jobWallets).map((pubkey: PublicKey) => {
return { isSigner: false, isWritable: true, pubkey };
})
)
.rpc();
return new LeaseAccount({ program, publicKey: leaseAccount.publicKey });
}
async getBalance(): Promise<number> {
// const [programStateAccount] = ProgramStateAccount.fromSeed(this.program);
// const switchTokenMint = await programStateAccount.getTokenMint();
// const mintData = await this.program.provider.connection.getAccountInfo(
// switchTokenMint.publicKey
// );
// const mintInfo = spl.TokenLayout.decode(mintData);
// const decimals = spl.u8.fromBuffer(mintInfo.decimals).toNumber();
const lease = await this.loadData();
const escrowInfo = await this.program.provider.connection.getAccountInfo(
lease.escrow
);
const data = Buffer.from(escrowInfo.data);
const accountInfo = spl.AccountLayout.decode(data);
const balance = (
spl.u64.fromBuffer(accountInfo.amount) as anchor.BN
).toNumber();
return balance; // / mintInfo.decimals;
}
/**
* Estimate the time remaining on a given lease
* @params void
* @returns number milliseconds left in lease (estimate)
*/
async estimatedLeaseTimeRemaining(): Promise<number> {
// get lease data for escrow + aggregator pubkeys
const lease = await this.loadData();
const aggregatorAccount = new AggregatorAccount({
program: this.program,
publicKey: lease.aggregator,
});
// get aggregator data for minUpdateDelaySeconds + batchSize + queue pubkey
const aggregator = await aggregatorAccount.loadData();
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: aggregator.queuePubkey,
});
const queue = await queueAccount.loadData();
const batchSize = aggregator.oracleRequestBatchSize + 1;
const minUpdateDelaySeconds = aggregator.minUpdateDelaySeconds * 1.5; // account for jitters with * 1.5
const updatesPerDay = (60 * 60 * 24) / minUpdateDelaySeconds;
const costPerDay = batchSize * queue.reward * updatesPerDay;
const oneDay = 24 * 60 * 60 * 1000; // ms in a day
const escrowInfo = await this.program.provider.connection.getAccountInfo(
lease.escrow
);
const data = Buffer.from(escrowInfo.data);
const accountInfo = spl.AccountLayout.decode(data);
const balance = (
spl.u64.fromBuffer(accountInfo.amount) as anchor.BN
).toNumber();
const endDate = new Date();
endDate.setTime(endDate.getTime() + (balance * oneDay) / costPerDay);
const timeLeft = endDate.getTime() - new Date().getTime();
return timeLeft;
}
/**
* Adds fund to a LeaseAccount. Note that funds can always be withdrawn by
* the withdraw authority if one was set on lease initialization.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
*/
async extend(params: LeaseExtendParams): Promise<TransactionSignature> {
const program = this.program;
const lease = await this.loadData();
const escrow = lease.escrow;
const queue = lease.queue;
const queueAccount = new OracleQueueAccount({ program, publicKey: queue });
const aggregator = lease.aggregator;
const aggregatorAccount = new AggregatorAccount({
program,
publicKey: aggregator,
});
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const switchTokenMint = await queueAccount.loadMint();
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
program,
queueAccount,
aggregatorAccount
);
const aggregatorData = await aggregatorAccount.loadData();
const jobPubkeys = aggregatorData.jobPubkeysData.slice(
0,
aggregatorData.jobPubkeysSize
);
const jobAccountDatas = await aggregatorAccount.loadJobAccounts();
const jobWallets: Array<PublicKey> = [];
const walletBumps: Array<number> = [];
for (const idx in jobAccountDatas) {
const jobAccountData = jobAccountDatas[idx];
const authority = jobAccountData.authority ?? PublicKey.default;
const [jobWallet, bump] = await PublicKey.findProgramAddress(
[
authority.toBuffer(),
spl.TOKEN_PROGRAM_ID.toBuffer(),
switchTokenMint.publicKey.toBuffer(),
],
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
jobWallets.push(jobWallet);
walletBumps.push(bump);
}
return program.methods
.leaseExtend({
loadAmount: params.loadAmount,
stateBump,
leaseBump,
walletBumps: Buffer.from(walletBumps),
})
.accounts({
lease: leaseAccount.publicKey,
aggregator,
queue,
funder: params.funder,
owner: params.funderAuthority.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
escrow,
programState: programStateAccount.publicKey,
mint: (await queueAccount.loadMint()).publicKey,
})
.signers([params.funderAuthority])
.remainingAccounts(
jobPubkeys.concat(jobWallets).map((pubkey: PublicKey) => {
return { isSigner: false, isWritable: true, pubkey };
})
)
.rpc();
}
/**
* Withdraw funds from a LeaseAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
*/
async withdraw(params: LeaseWithdrawParams): Promise<TransactionSignature> {
const program = this.program;
const lease = await this.loadData();
const escrow = lease.escrow;
const queue = lease.queue;
const queueAccount = new OracleQueueAccount({ program, publicKey: queue });
const aggregator = lease.aggregator;
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const switchTokenMint = await queueAccount.loadMint();
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
program,
queueAccount,
new AggregatorAccount({ program, publicKey: aggregator })
);
return program.methods
.leaseWithdraw({
amount: params.amount,
stateBump,
leaseBump,
})
.accounts({
lease: leaseAccount.publicKey,
escrow,
aggregator,
queue,
withdrawAuthority: params.withdrawAuthority.publicKey,
withdrawAccount: params.withdrawWallet,
tokenProgram: spl.TOKEN_PROGRAM_ID,
programState: programStateAccount.publicKey,
mint: (await queueAccount.loadMint()).publicKey,
})
.signers([params.withdrawAuthority])
.rpc();
}
}
/**
* Parameters for initializing a CrankAccount
*/
export interface CrankInitParams {
/**
* Buffer specifying crank name
*/
name?: Buffer;
/**
* Buffer specifying crank metadata
*/
metadata?: Buffer;
/**
* OracleQueueAccount for which this crank is associated
*/
queueAccount: OracleQueueAccount;
/**
* Optional max number of rows
*/
maxRows?: number;
}
/**
* Parameters for popping an element from a CrankAccount.
*/
export interface CrankPopParams {
/**
* Specifies the wallet to reward for turning the crank.
*/
payoutWallet: PublicKey;
/**
* The pubkey of the linked oracle queue.
*/
queuePubkey: PublicKey;
/**
* The pubkey of the linked oracle queue authority.
*/
queueAuthority: PublicKey;
/**
* Array of pubkeys to attempt to pop. If discluded, this will be loaded
* from the crank upon calling.
*/
readyPubkeys?: Array<PublicKey>;
/**
* Nonce to allow consecutive crank pops with the same blockhash.
*/
nonce?: number;
crank: any;
queue: any;
tokenMint: PublicKey;
failOpenOnMismatch?: boolean;
}
/**
* Parameters for pushing an element into a CrankAccount.
*/
export interface CrankPushParams {
/**
* Specifies the aggregator to push onto the crank.
*/
aggregatorAccount: AggregatorAccount;
}
/**
* Row structure of elements in the crank.
*/
export class CrankRow {
/**
* Aggregator account pubkey
*/
pubkey: PublicKey;
/**
* Next aggregator update timestamp to order the crank by
*/
nextTimestamp: anchor.BN;
static from(buf: Buffer): CrankRow {
const pubkey = new PublicKey(buf.slice(0, 32));
const nextTimestamp = new anchor.BN(buf.slice(32, 40), "le");
const res = new CrankRow();
res.pubkey = pubkey;
res.nextTimestamp = nextTimestamp;
return res;
}
}
/**
* A Switchboard account representing a crank of aggregators ordered by next update time.
*/
export class CrankAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* CrankAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Load and parse CrankAccount data based on the program IDL.
* @return CrankAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const crank: any = await this.program.account.crankAccountData.fetch(
this.publicKey
);
const pqData = [];
const buffer =
(
await this.program.provider.connection.getAccountInfo(crank.dataBuffer)
)?.data.slice(8) ?? Buffer.from("");
const rowSize = 40;
for (let i = 0; i < crank.pqSize * rowSize; i += rowSize) {
if (buffer.length - i < rowSize) {
break;
}
const rowBuf = buffer.slice(i, i + rowSize);
pqData.push(CrankRow.from(rowBuf));
}
crank.pqData = pqData;
crank.ebuf = undefined;
return crank;
}
/**
* Get the size of a CrankAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.crankAccountData.size;
}
/**
* Create and initialize the CrankAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated CrankAccount.
*/
static async create(
program: anchor.Program,
params: CrankInitParams
): Promise<CrankAccount> {
const payerKeypair = programWallet(program);
const crankAccount = anchor.web3.Keypair.generate();
const buffer = anchor.web3.Keypair.generate();
const size = program.account.crankAccountData.size;
params.maxRows = params.maxRows ?? 500;
const crankSize = params.maxRows * 40 + 8;
await program.methods
.crankInit({
name: (params.name ?? Buffer.from("")).slice(0, 32),
metadata: (params.metadata ?? Buffer.from("")).slice(0, 64),
crankSize: params.maxRows,
})
.accounts({
crank: crankAccount.publicKey,
queue: params.queueAccount.publicKey,
buffer: buffer.publicKey,
systemProgram: SystemProgram.programId,
payer: programWallet(program).publicKey,
})
.signers([crankAccount, buffer])
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: buffer.publicKey,
space: crankSize,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
crankSize
),
programId: program.programId,
}),
])
.rpc();
return new CrankAccount({ program, keypair: crankAccount });
}
/**
* Pushes a new aggregator onto the crank.
* @param aggregator The Aggregator account to push on the crank.
* @return TransactionSignature
*/
async push(params: CrankPushParams): Promise<TransactionSignature> {
const aggregatorAccount: AggregatorAccount = params.aggregatorAccount;
const crank = await this.loadData();
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: crank.queuePubkey,
});
const queue = await queueAccount.loadData();
const queueAuthority = queue.authority;
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
this.program,
queueAccount,
aggregatorAccount
);
let lease = null;
try {
lease = await leaseAccount.loadData();
} catch (_) {
throw new Error(
"A requested lease pda account has not been initialized."
);
}
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
queueAccount.publicKey,
aggregatorAccount.publicKey
);
try {
await permissionAccount.loadData();
} catch (_) {
throw new Error(
"A requested permission pda account has not been initialized."
);
}
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
return this.program.methods
.crankPush({
stateBump,
permissionBump,
})
.accounts({
crank: this.publicKey,
aggregator: aggregatorAccount.publicKey,
oracleQueue: queueAccount.publicKey,
queueAuthority,
permission: permissionAccount.publicKey,
lease: leaseAccount.publicKey,
escrow: lease.escrow,
programState: programStateAccount.publicKey,
dataBuffer: crank.dataBuffer,
})
.rpc();
}
/**
* Pops an aggregator from the crank.
* @param params
* @return TransactionSignature
*/
async popTxn(params: CrankPopParams): Promise<Transaction> {
const failOpenOnAccountMismatch = params.failOpenOnMismatch ?? false;
const next = params.readyPubkeys ?? (await this.peakNextReady(5));
if (next.length === 0) {
throw new Error("Crank is not ready to be turned.");
}
const remainingAccounts: Array<PublicKey> = [];
const leaseBumpsMap: Map<string, number> = new Map();
const permissionBumpsMap: Map<string, number> = new Map();
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: params.queuePubkey,
});
for (const row of next) {
const aggregatorAccount = new AggregatorAccount({
program: this.program,
publicKey: row,
});
const [leaseAccount, leaseBump] = LeaseAccount.fromSeed(
this.program,
queueAccount,
aggregatorAccount
);
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
params.queueAuthority,
params.queuePubkey,
row
);
const escrow = await spl.Token.getAssociatedTokenAddress(
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
spl.TOKEN_PROGRAM_ID,
params.tokenMint,
leaseAccount.publicKey,
true
);
remainingAccounts.push(aggregatorAccount.publicKey);
remainingAccounts.push(leaseAccount.publicKey);
remainingAccounts.push(escrow);
remainingAccounts.push(permissionAccount.publicKey);
leaseBumpsMap.set(row.toBase58(), leaseBump);
permissionBumpsMap.set(row.toBase58(), permissionBump);
}
remainingAccounts.sort((a: PublicKey, b: PublicKey) =>
a.toBuffer().compare(b.toBuffer())
);
const crank = params.crank;
const queue = params.queue;
const leaseBumps: Array<number> = [];
const permissionBumps: Array<number> = [];
// Map bumps to the index of their corresponding feeds.
for (const key of remainingAccounts) {
leaseBumps.push(leaseBumpsMap.get(key.toBase58()) ?? 0);
permissionBumps.push(permissionBumpsMap.get(key.toBase58()) ?? 0);
}
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const payerKeypair = programWallet(this.program);
let mint: PublicKey = queue.mint;
if (mint.equals(PublicKey.default)) {
mint = spl.NATIVE_MINT;
}
// const promises: Array<Promise<TransactionSignature>> = [];
return this.program.methods
.crankPop({
stateBump,
leaseBumps: Buffer.from(leaseBumps),
permissionBumps: Buffer.from(permissionBumps),
nonce: params.nonce ?? null,
failOpenOnAccountMismatch,
})
.accounts({
crank: this.publicKey,
oracleQueue: params.queuePubkey,
queueAuthority: params.queueAuthority,
programState: programStateAccount.publicKey,
payoutWallet: params.payoutWallet,
tokenProgram: spl.TOKEN_PROGRAM_ID,
crankDataBuffer: crank.dataBuffer,
queueDataBuffer: queue.dataBuffer,
mint,
})
.remainingAccounts(
remainingAccounts.map((pubkey: PublicKey) => {
return { isSigner: false, isWritable: true, pubkey };
})
)
.signers([payerKeypair])
.transaction();
}
/**
* Pops an aggregator from the crank.
* @param params
* @return TransactionSignature
*/
async pop(params: CrankPopParams): Promise<TransactionSignature> {
const payerKeypair = programWallet(this.program);
return sendAndConfirmTransaction(
this.program.provider.connection,
await this.popTxn(params),
[payerKeypair]
);
}
/**
* Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
* @param n The limit of pubkeys to return.
* @return Pubkey list of Aggregators and next timestamp to be popped, ordered by timestamp.
*/
async peakNextWithTime(n: number): Promise<Array<CrankRow>> {
const crank = await this.loadData();
const items = crank.pqData
.slice(0, crank.pqSize)
.sort((a: CrankRow, b: CrankRow) => a.nextTimestamp.sub(b.nextTimestamp))
.slice(0, n);
return items;
}
/**
* Get an array of the next readily updateable aggregator pubkeys to be popped
* from the crank, limited by n
* @param n The limit of pubkeys to return.
* @return Pubkey list of Aggregator pubkeys.
*/
async peakNextReady(n?: number): Promise<Array<PublicKey>> {
const now = Math.floor(+new Date() / 1000);
const crank = await this.loadData();
n = n ?? crank.pqSize;
const items = crank.pqData
.slice(0, crank.pqSize)
.filter((row: CrankRow) => now >= row.nextTimestamp.toNumber())
.sort((a: CrankRow, b: CrankRow) => a.nextTimestamp.sub(b.nextTimestamp))
.slice(0, n)
.map((item: CrankRow) => item.pubkey);
return items;
}
/**
* Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
* @param n The limit of pubkeys to return.
* @return Pubkey list of Aggregators next up to be popped.
*/
async peakNext(n: number): Promise<Array<PublicKey>> {
const crank = await this.loadData();
const items = crank.pqData
.slice(0, crank.pqSize)
.sort((a: CrankRow, b: CrankRow) => a.nextTimestamp.sub(b.nextTimestamp))
.map((item: CrankRow) => item.pubkey)
.slice(0, n);
return items;
}
}
/**
* Parameters for an OracleInit request.
*/
export interface OracleInitParams {
/**
* Buffer specifying oracle name
*/
name?: Buffer;
/**
* Buffer specifying oracle metadata
*/
metadata?: Buffer;
/**
* If included, this keypair will be the oracle authority.
*/
oracleAuthority?: Keypair;
/**
* Specifies the oracle queue to associate with this OracleAccount.
*/
queueAccount: OracleQueueAccount;
}
/**
* Parameters for an OracleWithdraw request.
*/
export interface OracleWithdrawParams {
/**
* Amount to withdraw
*/
amount: anchor.BN;
/**
* Token Account to withdraw to
*/
withdrawAccount: PublicKey;
/**
* Oracle authority keypair.
*/
oracleAuthority: Keypair;
}
/**
* A Switchboard account representing an oracle account and its associated queue
* and escrow account.
*/
export class OracleAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* OracleAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Load and parse OracleAccount data based on the program IDL.
* @return OracleAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const item: any = await this.program.account.oracleAccountData.fetch(
this.publicKey
);
item.ebuf = undefined;
return item;
}
/**
* Get the size of an OracleAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.oracleAccountData.size;
}
/**
* Create and initialize the OracleAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated OracleAccount.
*/
static async create(
program: anchor.Program,
params: OracleInitParams
): Promise<OracleAccount> {
const payerKeypair = programWallet(program);
const authorityKeypair = params.oracleAuthority ?? payerKeypair;
const size = program.account.oracleAccountData.size;
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const mint = await params.queueAccount.loadMint();
const wallet = await mint.createAccount(programWallet(program).publicKey);
await mint.setAuthority(
wallet,
programStateAccount.publicKey,
"AccountOwner",
payerKeypair,
[]
);
const [oracleAccount, oracleBump] = OracleAccount.fromSeed(
program,
params.queueAccount,
wallet
);
await program.methods
.oracleInit({
name: (params.name ?? Buffer.from("")).slice(0, 32),
metadata: (params.metadata ?? Buffer.from("")).slice(0, 128),
stateBump,
oracleBump,
})
.accounts({
oracle: oracleAccount.publicKey,
oracleAuthority: authorityKeypair.publicKey,
queue: params.queueAccount.publicKey,
wallet,
programState: programStateAccount.publicKey,
systemProgram: SystemProgram.programId,
payer: programWallet(program).publicKey,
})
.rpc();
return new OracleAccount({ program, publicKey: oracleAccount.publicKey });
}
static decode(
program: anchor.Program,
accountInfo: AccountInfo<Buffer>
): any {
const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "OracleAccountData";
const data = coder.decode(key, accountInfo?.data!);
return data;
}
/**
* Constructs OracleAccount from the static seed from which it was generated.
* @return OracleAccount and PDA bump tuple.
*/
static fromSeed(
program: anchor.Program,
queueAccount: OracleQueueAccount,
wallet: PublicKey
): [OracleAccount, number] {
const [oraclePubkey, oracleBump] =
anchor.utils.publicKey.findProgramAddressSync(
[
Buffer.from("OracleAccountData"),
queueAccount.publicKey.toBuffer(),
wallet.toBuffer(),
],
program.programId
);
return [
new OracleAccount({ program, publicKey: oraclePubkey }),
oracleBump,
];
}
/**
* Inititates a heartbeat for an OracleAccount, signifying oracle is still healthy.
* @return TransactionSignature.
*/
async heartbeat(authority: Keypair): Promise<TransactionSignature> {
const payerKeypair = programWallet(this.program);
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: (await this.loadData()).queuePubkey,
});
const queue = await queueAccount.loadData();
let lastPubkey = this.publicKey;
if (queue.size !== 0) {
lastPubkey = queue.queue[queue.gcIdx];
}
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queue.authority,
queueAccount.publicKey,
this.publicKey
);
try {
await permissionAccount.loadData();
} catch (_) {
throw new Error(
"A requested permission pda account has not been initialized."
);
}
const oracle = await this.loadData();
assert(this.publicKey !== undefined);
assert(payerKeypair.publicKey !== undefined);
assert(oracle.tokenAccount !== undefined);
assert(lastPubkey !== undefined);
assert(queueAccount.publicKey !== undefined);
assert(queueAccount.publicKey !== undefined);
assert(permissionAccount.publicKey !== undefined);
assert(queue.dataBuffer !== undefined);
return this.program.methods
.oracleHeartbeat({
permissionBump,
})
.accounts({
oracle: this.publicKey,
oracleAuthority: payerKeypair.publicKey,
tokenAccount: oracle.tokenAccount,
gcOracle: lastPubkey,
oracleQueue: queueAccount.publicKey,
permission: permissionAccount.publicKey,
dataBuffer: queue.dataBuffer,
})
.signers([authority])
.rpc();
}
/**
/**
* Inititates a heartbeat for an OracleAccount, signifying oracle is still healthy.
* @return TransactionSignature.
*/
async heartbeatTx(): Promise<Transaction> {
const payerKeypair = programWallet(this.program);
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: (await this.loadData()).queuePubkey,
});
const queue = await queueAccount.loadData();
let lastPubkey = this.publicKey;
if (queue.size !== 0) {
lastPubkey = queue.queue[queue.gcIdx];
}
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queue.authority,
queueAccount.publicKey,
this.publicKey
);
try {
await permissionAccount.loadData();
} catch (_) {
throw new Error(
"A requested permission pda account has not been initialized."
);
}
const oracle = await this.loadData();
return this.program.methods
.oracleHeartbeat({
permissionBump,
})
.accounts({
oracle: this.publicKey,
oracleAuthority: payerKeypair.publicKey,
tokenAccount: oracle.tokenAccount,
gcOracle: lastPubkey,
oracleQueue: queueAccount.publicKey,
permission: permissionAccount.publicKey,
dataBuffer: queue.dataBuffer,
})
.signers([this.keypair])
.transaction();
}
/**
* Withdraw stake and/or rewards from an OracleAccount.
*/
async withdraw(params: OracleWithdrawParams): Promise<TransactionSignature> {
const payerKeypair = programWallet(this.program);
const oracle = await this.loadData();
const queuePubkey = oracle.queuePubkey;
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: queuePubkey,
});
const queueAuthority = (await queueAccount.loadData()).authority;
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
queueAccount.publicKey,
this.publicKey
);
return this.program.methods
.oracleWithdraw({
permissionBump,
stateBump,
amount: params.amount,
})
.accounts({
oracle: this.publicKey,
oracleAuthority: params.oracleAuthority.publicKey,
tokenAccount: oracle.tokenAccount,
withdrawAccount: params.withdrawAccount,
oracleQueue: queueAccount.publicKey,
permission: permissionAccount.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
programState: stateAccount.publicKey,
systemProgram: SystemProgram.programId,
payer: programWallet(this.program).publicKey,
})
.signers([params.oracleAuthority])
.rpc();
}
async getBalance(): Promise<number> {
const oracle = await this.loadData();
const escrowInfo = await this.program.provider.connection.getAccountInfo(
oracle.tokenAccount
);
const data = Buffer.from(escrowInfo.data);
const accountInfo = spl.AccountLayout.decode(data);
const balance = (
spl.u64.fromBuffer(accountInfo.amount) as anchor.BN
).toNumber();
return balance; // / mintInfo.decimals;
}
}
export interface Callback {
programId: PublicKey;
accounts: Array<AccountMeta>;
ixData: Buffer;
}
/**
* Parameters for a VrfInit request.
*/
export interface VrfInitParams {
/**
* Vrf account authority to configure the account
*/
authority: PublicKey;
queue: OracleQueueAccount;
callback: Callback;
/**
* Keypair to use for the vrf account.
*/
keypair: Keypair;
}
/**
* Parameters for a VrfSetCallback request.
*/
export interface VrfSetCallbackParams {
authority: Keypair;
cpiProgramId: PublicKey;
accountList: Array<AccountMeta>;
instruction: Buffer;
}
export interface VrfProveAndVerifyParams {
proof: Buffer;
oracleAccount: OracleAccount;
oracleAuthority: Keypair;
skipPreflight: boolean;
}
export interface VrfRequestRandomnessParams {
authority: Keypair;
payer: PublicKey;
payerAuthority: Keypair;
}
export interface VrfProveParams {
proof: Buffer;
oracleAccount: OracleAccount;
oracleAuthority: Keypair;
}
/**
* A Switchboard VRF account.
*/
export class VrfAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* CrankAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Load and parse VrfAccount data based on the program IDL.
* @return VrfAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const vrf: any = await this.program.account.vrfAccountData.fetch(
this.publicKey
);
vrf.ebuf = undefined;
vrf.builders = vrf.builders.slice(0, vrf.buildersLen);
return vrf;
}
/**
* Get the size of a VrfAccount on chain.
* @return size.
*/
size(): number {
return this.program.account.vrfAccountData.size;
}
/**
* Create and initialize the VrfAccount.
* @param program Switchboard program representation holding connection and IDL.
* @param params.
* @return newly generated VrfAccount.
*/
static async create(
program: anchor.Program,
params: VrfInitParams
): Promise<VrfAccount> {
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const keypair = params.keypair;
const size = program.account.vrfAccountData.size;
const switchTokenMint = await params.queue.loadMint();
const escrow = await spl.Token.getAssociatedTokenAddress(
switchTokenMint.associatedProgramId,
switchTokenMint.programId,
switchTokenMint.publicKey,
keypair.publicKey,
true
);
try {
await (switchTokenMint as any).createAssociatedTokenAccountInternal(
keypair.publicKey,
escrow
);
} catch (e) {
console.log(e);
}
await switchTokenMint.setAuthority(
escrow,
programStateAccount.publicKey,
"AccountOwner",
keypair,
[]
);
await program.methods
.vrfInit({
stateBump,
callback: params.callback,
})
.accounts({
vrf: keypair.publicKey,
escrow,
authority: params.authority ?? keypair.publicKey,
oracleQueue: params.queue.publicKey,
programState: programStateAccount.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
})
.preInstructions([
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: keypair.publicKey,
space: size,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
size
),
programId: program.programId,
}),
])
.signers([keypair])
.rpc();
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
*/
async requestRandomness(params: VrfRequestRandomnessParams) {
const vrf = await this.loadData();
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: vrf.oracleQueue,
});
const queue = await queueAccount.loadData();
const queueAuthority = queue.authority;
const dataBuffer = queue.dataBuffer;
const escrow = vrf.escrow;
const payer = params.payer;
const [stateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
queueAccount.publicKey,
this.publicKey
);
try {
await permissionAccount.loadData();
} catch (_) {
throw new Error(
"A requested permission pda account has not been initialized."
);
}
const tokenProgram = spl.TOKEN_PROGRAM_ID;
const recentBlockhashes = SYSVAR_RECENT_BLOCKHASHES_PUBKEY;
await this.program.methods
.vrfRequestRandomness({
stateBump,
permissionBump,
})
.accounts({
authority: params.authority.publicKey,
vrf: this.publicKey,
oracleQueue: queueAccount.publicKey,
queueAuthority,
dataBuffer,
permission: permissionAccount.publicKey,
escrow,
payerWallet: payer,
payerAuthority: params.payerAuthority.publicKey,
recentBlockhashes,
programState: stateAccount.publicKey,
tokenProgram,
})
.signers([params.authority, params.payerAuthority])
.rpc();
}
async prove(params: VrfProveParams): Promise<TransactionSignature> {
const vrf = await this.loadData();
let idx = -1;
let producerKey = PublicKey.default;
for (idx = 0; idx < vrf.buildersLen; ++idx) {
const builder = vrf.builders[idx];
producerKey = builder.producer;
if (producerKey.equals(params.oracleAccount.publicKey)) {
break;
}
}
if (idx === vrf.buildersLen) {
throw new Error("OracleProofRequestNotFoundError");
}
return this.program.methods
.vrfProve({
proof: params.proof,
idx,
})
.accounts({
vrf: this.publicKey,
oracle: producerKey,
randomnessProducer: params.oracleAuthority.publicKey,
})
.signers([params.oracleAuthority])
.rpc();
}
async verify(
oracle: OracleAccount,
tryCount = 278
): Promise<Array<TransactionSignature>> {
const skipPreflight = true;
const txs: Array<any> = [];
const vrf = await this.loadData();
const idx = vrf.builders.find((builder: any) =>
oracle.publicKey.equals(builder.producer)
);
if (idx === -1) {
throw new Error("OracleNotFoundError");
}
const counter = 0;
const remainingAccounts = vrf.callback.accounts.slice(
0,
vrf.callback.accountsLen
);
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const oracleData = await oracle.loadData();
const oracleWallet = oracleData.tokenAccount;
const oracleAuthority: PublicKey = oracleData.oracleAuthority;
const instructions = [];
const tx = new Transaction();
for (let i = 0; i < tryCount; ++i) {
txs.push({
tx: await this.program.methods
.vrfVerify({
nonce: i,
stateBump,
idx,
})
.accounts({
vrf: this.publicKey,
callbackPid: vrf.callback.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
escrow: vrf.escrow,
programState: programStateAccount.publicKey,
oracle: oracle.publicKey,
oracleAuthority,
oracleWallet,
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.remainingAccounts(remainingAccounts)
.transaction(),
});
// try {
// tx.add(newTx);
// } catch (e) {
// txs.push({ tx });
// tx = newTx;
// }
// txs.push(newTx);
}
// txs.push({ tx });
return sendAll(this.program.provider, txs, [], skipPreflight);
}
/**
* Attempt the maximum amount of turns remaining on the vrf verify crank.
* This will automatically call the vrf callback (if set) when completed.
*/
async proveAndVerify(
params: VrfProveAndVerifyParams,
tryCount = 278
): Promise<Array<TransactionSignature>> {
const skipPreflight = params.skipPreflight;
const oracle = params.oracleAccount;
const txs: Array<any> = [];
const vrf = await this.loadData();
const idx = vrf.builders.find((builder: any) =>
oracle.publicKey.equals(builder.producer)
);
if (idx === -1) {
throw new Error("OracleNotFoundError");
}
const counter = 0;
const remainingAccounts = vrf.callback.accounts.slice(
0,
vrf.callback.accountsLen
);
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const oracleData = await oracle.loadData();
const oracleWallet = oracleData.tokenAccount;
const oracleAuthority: PublicKey = oracleData.oracleAuthority;
const instructions = [];
const tx = new Transaction();
for (let i = 0; i < tryCount; ++i) {
txs.push({
tx: await this.program.methods
.vrfProveAndVerify({
nonce: i,
stateBump,
idx,
proof: params.proof,
})
.accounts({
vrf: this.publicKey,
callbackPid: vrf.callback.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
escrow: vrf.escrow,
programState: programStateAccount.publicKey,
oracle: oracle.publicKey,
oracleAuthority,
oracleWallet,
instructionsSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.remainingAccounts(remainingAccounts)
.signers([params.oracleAuthority])
.transaction(),
});
// try {
// tx.add(newTx);
// } catch (e) {
// txs.push({ tx });
// tx = newTx;
// }
// txs.push(newTx);
}
// txs.push({ tx });
return sendAll(
this.program.provider,
txs,
[params.oracleAuthority],
skipPreflight
);
}
}
export class BufferRelayerAccount {
program: anchor.Program;
publicKey: PublicKey;
keypair?: Keypair;
/**
* CrankAccount constructor
* @param params initialization params.
*/
public constructor(params: AccountParams) {
if (params.keypair === undefined && params.publicKey === undefined) {
throw new Error(
`${this.constructor.name}: User must provide either a publicKey or keypair for account use.`
);
}
if (params.keypair !== undefined && params.publicKey !== undefined) {
if (!params.publicKey.equals(params.keypair.publicKey)) {
throw new Error(
`${this.constructor.name}: provided pubkey and keypair mismatch.`
);
}
}
this.program = params.program;
this.keypair = params.keypair;
this.publicKey = params.publicKey ?? this.keypair.publicKey;
}
/**
* Load and parse BufferRelayerAccount data based on the program IDL.
* @return BufferRelayerAccount data parsed in accordance with the
* Switchboard IDL.
*/
async loadData(): Promise<any> {
const data: any = await this.program.account.bufferRelayerAccountData.fetch(
this.publicKey
);
data.ebuf = undefined;
return data;
}
size(): number {
return 4092;
}
static async create(
program: anchor.Program,
params: {
name: Buffer;
minUpdateDelaySeconds: number;
queueAccount: OracleQueueAccount;
authority: PublicKey;
jobAccount: JobAccount;
}
): Promise<BufferRelayerAccount> {
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(program);
const switchTokenMint = await params.queueAccount.loadMint();
const keypair = Keypair.generate();
const escrow = await spl.Token.getAssociatedTokenAddress(
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
spl.TOKEN_PROGRAM_ID,
switchTokenMint.publicKey,
keypair.publicKey
);
const size = 2048;
const payer = programWallet(program);
await program.rpc.bufferRelayerInit(
{
name: params.name.slice(0, 32),
minUpdateDelaySeconds: params.minUpdateDelaySeconds,
stateBump,
},
{
accounts: {
bufferRelayer: keypair.publicKey,
escrow,
authority: params.authority,
queue: params.queueAccount.publicKey,
job: params.jobAccount.publicKey,
programState: programStateAccount.publicKey,
mint: switchTokenMint.publicKey,
payer: payer.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
rent: new PublicKey("SysvarRent111111111111111111111111111111111"),
},
instructions: [
anchor.web3.SystemProgram.createAccount({
fromPubkey: programWallet(program).publicKey,
newAccountPubkey: keypair.publicKey,
space: size,
lamports:
await program.provider.connection.getMinimumBalanceForRentExemption(
size
),
programId: program.programId,
}),
],
signers: [keypair],
}
);
return new BufferRelayerAccount({ program, keypair });
}
async openRound(): Promise<TransactionSignature> {
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const relayerData = await this.loadData();
const queue = relayerData.queuePubkey;
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: queue,
});
const switchTokenMint = await queueAccount.loadMint();
await switchTokenMint.getOrCreateAssociatedAccountInfo(
programWallet(this.program).publicKey
);
const source = await spl.Token.getAssociatedTokenAddress(
spl.ASSOCIATED_TOKEN_PROGRAM_ID,
spl.TOKEN_PROGRAM_ID,
switchTokenMint.publicKey,
programWallet(this.program).publicKey,
true
);
const bufferRelayer = this.publicKey;
const escrow = relayerData.escrow;
const queueData = await queueAccount.loadData();
const queueAuthority = queueData.authority;
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
queueAccount.publicKey,
this.publicKey
);
const payer = programWallet(this.program);
const transferIx = spl.Token.createTransferInstruction(
spl.TOKEN_PROGRAM_ID,
source,
escrow,
programWallet(this.program).publicKey,
[],
queueData.reward.toNumber()
);
const openRoundIx = this.program.instruction.bufferRelayerOpenRound(
{
stateBump,
permissionBump,
},
{
accounts: {
bufferRelayer,
oracleQueue: queueAccount.publicKey,
dataBuffer: queueData.dataBuffer,
queueAuthority: queueData.authority,
permission: permissionAccount.publicKey,
escrow,
programState: programStateAccount.publicKey,
job: relayerData.jobPubkey,
},
}
);
const tx = new Transaction();
tx.add(transferIx);
tx.add(openRoundIx);
const connection = (this.program.provider as anchor.AnchorProvider)
.connection;
return sendAndConfirmTransaction(connection, tx, [
programWallet(this.program),
]);
}
async saveResult(params: {
oracleAuthority: Keypair;
result: Buffer;
success: boolean;
}): Promise<TransactionSignature> {
const [programStateAccount, stateBump] = ProgramStateAccount.fromSeed(
this.program
);
const relayerData = await this.loadData();
const queue = new PublicKey(relayerData.queuePubkey);
const queueAccount = new OracleQueueAccount({
program: this.program,
publicKey: queue!,
});
const bufferRelayer = this.publicKey;
const escrow = relayerData.escrow;
const queueData = await queueAccount.loadData();
const queueAuthority = queueData.authority;
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queueAuthority,
queueAccount.publicKey,
this.publicKey
);
const oracleAccount = new OracleAccount({
program: this.program,
publicKey: relayerData.currentRound.oraclePubkey,
});
const oracleData = await oracleAccount.loadData();
console.log("!!!!");
return this.program.rpc.bufferRelayerSaveResult(
{
stateBump,
permissionBump,
result: params.result,
success: params.success,
},
{
accounts: {
bufferRelayer,
oracleAuthority: params.oracleAuthority.publicKey,
oracle: relayerData.currentRound.oraclePubkey,
oracleQueue: queueAccount.publicKey,
dataBuffer: queueData.dataBuffer,
queueAuthority: queueData.authority,
permission: permissionAccount.publicKey,
escrow,
programState: programStateAccount.publicKey,
oracleWallet: oracleData.tokenAccount,
tokenProgram: spl.TOKEN_PROGRAM_ID,
},
}
);
}
}
export async function sendAll(
provider: anchor.Provider,
reqs: Array<any>,
signers: Array<Keypair>,
skipPreflight: boolean
): Promise<Array<TransactionSignature>> {
const res: Array<TransactionSignature> = [];
try {
const opts = (provider as anchor.AnchorProvider).opts;
// TODO: maybe finalized
const blockhash = await provider.connection.getLatestBlockhash("confirmed");
let txs = reqs.map((r: any) => {
if (r === null || r === undefined) return new Transaction();
const tx = r.tx;
let signers = r.signers;
if (signers === undefined) {
signers = [];
}
tx.feePayer = (provider as anchor.AnchorProvider).wallet.publicKey;
tx.recentBlockhash = blockhash.blockhash;
signers
.filter((s: any): s is Signer => s !== undefined)
.forEach((kp: any) => {
tx.partialSign(kp);
});
return tx;
});
txs = await packTransactions(
provider.connection,
txs,
signers,
(provider as anchor.AnchorProvider).wallet.publicKey
);
const signedTxs = await (
provider as anchor.AnchorProvider
).wallet.signAllTransactions(txs);
const promises = [];
for (let k = 0; k < txs.length; k += 1) {
const tx = signedTxs[k];
const rawTx = tx.serialize();
promises.push(
provider.connection.sendRawTransaction(rawTx, {
skipPreflight,
maxRetries: 10,
})
);
}
return await Promise.all(promises);
} catch (e) {
console.log(e);
}
return res;
}
/**
* Pack instructions into transactions as tightly as possible
* @param instructions Instructions or Grouping of Instructions to pack down into transactions.
* Arrays of instructions will be grouped into the same tx.
* NOTE: this will break if grouping is too large for a single tx
* @param feePayer Optional feepayer
* @param recentBlockhash Optional blockhash
* @returns Transaction[]
*/
export function packInstructions(
instructions: (TransactionInstruction | TransactionInstruction[])[],
feePayer: PublicKey = PublicKey.default,
recentBlockhash: string = PublicKey.default.toBase58()
): Transaction[] {
const packed: Transaction[] = [];
let currentTransaction = new Transaction();
currentTransaction.recentBlockhash = recentBlockhash;
currentTransaction.feePayer = feePayer;
const encodeLength = (bytes: Array<number>, len: number) => {
let rem_len = len;
for (;;) {
let elem = rem_len & 0x7f;
rem_len >>= 7;
if (rem_len == 0) {
bytes.push(elem);
break;
} else {
elem |= 0x80;
bytes.push(elem);
}
}
};
for (const ixGroup of instructions) {
const ixs = Array.isArray(ixGroup) ? ixGroup : [ixGroup];
for (const ix of ixs) {
// add the new transaction
currentTransaction.add(ix);
}
const sigCount: number[] = [];
encodeLength(sigCount, currentTransaction.signatures.length);
if (
anchor.web3.PACKET_DATA_SIZE <=
currentTransaction.serializeMessage().length +
currentTransaction.signatures.length * 64 +
sigCount.length
) {
// If the aggregator transaction fits, it will serialize without error. We can then push it ahead no problem
const trimmedInstructions = ixs
.map(() => currentTransaction.instructions.pop()!)
.reverse();
// Every serialize adds the instruction signatures as dependencies
currentTransaction.signatures = [];
const overflowInstructions = trimmedInstructions;
// add the capped transaction to our transaction - only push it if it works
packed.push(currentTransaction);
currentTransaction = new Transaction();
currentTransaction.recentBlockhash = recentBlockhash;
currentTransaction.feePayer = feePayer;
currentTransaction.instructions = overflowInstructions;
const newsc: number[] = [];
encodeLength(newsc, currentTransaction.signatures.length);
if (
anchor.web3.PACKET_DATA_SIZE <=
currentTransaction.serializeMessage().length +
currentTransaction.signatures.length * 64 +
newsc.length
) {
throw new Error(
"Instruction packing error: a grouping of instructions must be able to fit into a single transaction"
);
}
}
}
packed.push(currentTransaction);
return packed;
}
/**
* Repack Transactions and sign them
* @param connection Web3.js Connection
* @param transactions Transactions to repack
* @param signers Signers for each transaction
*/
export async function packTransactions(
connection: anchor.web3.Connection,
transactions: Transaction[],
signers: Keypair[],
feePayer: PublicKey
): Promise<Transaction[]> {
const instructions = transactions.map((t) => t.instructions).flat();
const txs = packInstructions(instructions, feePayer);
const { blockhash } = await connection.getLatestBlockhash("confirmed");
txs.forEach((t) => {
t.recentBlockhash = blockhash;
});
return signTransactions(txs, signers);
}
/**
* Sign transactions with correct signers
* @param transactions array of transactions to sign
* @param signers array of keypairs to sign the array of transactions with
* @returns transactions signed
*/
export function signTransactions(
transactions: Transaction[],
signers: Keypair[]
): Transaction[] {
// Sign with all the appropriate signers
for (const transaction of transactions) {
// Get pubkeys of signers needed
const sigsNeeded = transaction.instructions
.map((instruction) => {
const signers = instruction.keys.filter((meta) => meta.isSigner);
return signers.map((signer) => signer.pubkey);
})
.flat();
// Get matching signers in our supplied array
const currentSigners = signers.filter((signer) =>
Boolean(sigsNeeded.find((sig) => sig.equals(signer.publicKey)))
);
// Sign all transactions
for (const signer of currentSigners) {
transaction.partialSign(signer);
}
}
return transactions;
}
// Create mint with a pre-generated keypair.
export async function createMint(
connection: Connection,
payer: Signer,
mintAuthority: PublicKey,
freezeAuthority: PublicKey | null,
decimals: number,
programId: PublicKey,
mintAccount: Keypair
): Promise<spl.Token> {
const tkn = new spl.Token(
connection,
mintAccount.publicKey,
programId,
payer
);
// Allocate memory for the account
const balanceNeeded = await spl.Token.getMinBalanceRentForExemptMint(
connection
);
const transaction = new Transaction();
transaction.add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintAccount.publicKey,
lamports: balanceNeeded,
space: spl.MintLayout.span,
programId,
})
);
transaction.add(
spl.Token.createInitMintInstruction(
programId,
mintAccount.publicKey,
decimals,
mintAuthority,
freezeAuthority
)
);
// Send the two instructions
await sendAndConfirmTransaction(connection, transaction, [
payer,
mintAccount,
]);
return tkn;
}
export function programWallet(program: anchor.Program): Keypair {
return ((program.provider as anchor.AnchorProvider).wallet as AnchorWallet)
.payer;
}
function safeDiv(number_: Big, denominator: Big, decimals = 20): Big {
const oldDp = Big.DP;
Big.DP = decimals;
const result = number_.div(denominator);
Big.DP = oldDp;
return result;
}
export class AnchorWallet implements anchor.Wallet {
constructor(readonly payer: Keypair) {
this.payer = payer;
}
async signTransaction(tx: Transaction): Promise<Transaction> {
tx.partialSign(this.payer);
return tx;
}
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
return txs.map((t) => {
t.partialSign(this.payer);
return t;
});
}
get publicKey(): PublicKey {
return this.payer.publicKey;
}
}