import * as errors from "../errors.js"; import { AggregatorAccountData, AggregatorAccountDataFields, AggregatorAccountDataJSON, } from "../generated/oracle-program/accounts/AggregatorAccountData.js"; import { JobAccountData, JobAccountDataJSON, } from "../generated/oracle-program/accounts/JobAccountData.js"; import { LeaseAccountData, LeaseAccountDataJSON, } from "../generated/oracle-program/accounts/LeaseAccountData.js"; import { OracleAccountData } from "../generated/oracle-program/accounts/OracleAccountData.js"; import { OracleQueueAccountData, OracleQueueAccountDataJSON, } from "../generated/oracle-program/accounts/OracleQueueAccountData.js"; import { PermissionAccountData, PermissionAccountDataJSON, } from "../generated/oracle-program/accounts/PermissionAccountData.js"; import * as ix from "../generated/oracle-program/instructions/index.js"; import { AggregatorHistoryRow, AggregatorResolutionMode, AggregatorResolutionModeKind, BorshDecimal, } from "../generated/oracle-program/types/index.js"; import { SwitchboardDecimal } from "../generated/oracle-program/types/SwitchboardDecimal.js"; import { PermitOracleQueueUsage } from "../generated/oracle-program/types/SwitchboardPermission.js"; import { SwitchboardProgram } from "../SwitchboardProgram.js"; import { SendTransactionObjectOptions, TransactionObject, TransactionObjectOptions, TransactionPackOptions, } from "../TransactionObject.js"; import { Account, OnAccountChangeCallback } from "./account.js"; import { AggregatorHistoryBuffer } from "./aggregatorHistoryBuffer.js"; import { CrankAccount } from "./crankAccount.js"; import { JobAccount } from "./jobAccount.js"; import { LeaseAccount, LeaseExtendParams } from "./leaseAccount.js"; import { OracleAccount } from "./oracleAccount.js"; import { PermissionAccount } from "./permissionAccount.js"; import { QueueAccount } from "./queueAccount.js"; import * as anchor from "@coral-xyz/anchor"; import * as spl from "@solana/spl-token"; import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { AccountInfo, AccountMeta, Commitment, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, TransactionInstruction, TransactionSignature, } from "@solana/web3.js"; import { Big, BN, OracleJob, promiseWithTimeout, toUtf8, } from "@switchboard-xyz/common"; import assert from "assert"; import crypto, { createHash } from "crypto"; /** * Account type holding a data feed's update configuration, job accounts, and its current result. * * Data: {@linkcode AggregatorAccountData} * * Result: {@linkcode SwitchboardDecimal} * * HistoryBuffer?: Array<{@linkcode AggregatorHistoryRow}> * * An aggregator account belongs to a single {@linkcode QueueAccount} but can later be transferred by the aggregator's authority. In order for an {@linkcode OracleAccount} to respond to an aggregator's update request, the aggregator must initialize a {@linkcode PermissionAccount} and {@linkcode LeaseAccount}. These will need to be recreated when transferring queues. * * Optionally, An aggregator can be pushed onto a {@linkcode CrankAccount} in order to be updated * * Optionally, an aggregator can add a history buffer to store the last N historical samples along with their update timestamp. */ export class AggregatorAccount extends Account { static accountName = "AggregatorAccountData"; public history?: AggregatorHistoryBuffer; /** * Returns the aggregator's name buffer in a stringified format. */ public static getName = (aggregator: AggregatorAccountData) => toUtf8(aggregator.name); /** * Returns the aggregator's metadata buffer in a stringified format. */ public static getMetadata = (aggregator: AggregatorAccountData) => toUtf8(aggregator.metadata); public static size = 3851; /** * Get the size of an {@linkcode AggregatorAccount} on-chain. */ public size = this.program.account.aggregatorAccountData.size; public decode(data: Buffer): AggregatorAccountData { try { return AggregatorAccountData.decode(data); } catch { return this.program.coder.decode( AggregatorAccount.accountName, data ); } } /** * Return an aggregator account state initialized to the default values. */ public static default(): AggregatorAccountData { const buffer = Buffer.alloc(AggregatorAccount.size, 0); AggregatorAccountData.discriminator.copy(buffer, 0); return AggregatorAccountData.decode(buffer); } /** * Create a mock account info for a given aggregator config. Useful for test integrations. */ public static createMock( programId: PublicKey, data: Partial, options?: { lamports?: number; rentEpoch?: number; } ): AccountInfo { const fields: AggregatorAccountDataFields = { ...AggregatorAccount.default(), ...data, // any cleanup actions here }; const state = new AggregatorAccountData(fields); const buffer = Buffer.alloc(AggregatorAccount.size, 0); AggregatorAccountData.discriminator.copy(buffer, 0); AggregatorAccountData.layout.encode(state, buffer, 8); return { executable: false, owner: programId, lamports: options?.lamports ?? 1 * LAMPORTS_PER_SOL, data: buffer, rentEpoch: options?.rentEpoch ?? 0, }; } /** * Invoke a callback each time an AggregatorAccount's data has changed on-chain. * @param callback - the callback invoked when the aggregator state changes * @param commitment - optional, the desired transaction finality. defaults to 'confirmed' * @returns the websocket subscription id */ public onChange( callback: OnAccountChangeCallback, commitment: Commitment = "confirmed" ): number { return this.program.connection.onAccountChange( this.publicKey, (accountInfo) => { callback(this.decode(accountInfo.data)); }, commitment ); } /** * Retrieve and decode the {@linkcode AggregatorAccountData} stored in this account. */ public async loadData(): Promise { const data = await AggregatorAccountData.fetch( this.program, this.publicKey ); if (data === null) throw new errors.AccountNotFoundError("Aggregator", this.publicKey); this.history = AggregatorHistoryBuffer.fromAggregator(this.program, data); return data; } public get slidingWindowKey(): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("SlidingResultAccountData"), this.publicKey.toBytes()], this.program.programId )[0]; } /** Load an existing AggregatorAccount with its current on-chain state */ public static async load( program: SwitchboardProgram, publicKey: PublicKey | string ): Promise<[AggregatorAccount, AggregatorAccountData]> { const account = new AggregatorAccount( program, typeof publicKey === "string" ? new PublicKey(publicKey) : publicKey ); const state = await account.loadData(); return [account, state]; } /** * Creates a transaction object with aggregatorInit instructions. * @param program The SwitchboardProgram. * @param payer The account that will pay for the new accounts. * @param params aggregator configuration parameters. * @return {@linkcode TransactionObject} that will create the aggregatorAccount. * * Basic usage example: * * ```ts * import {AggregatorAccount} from '@switchboard-xyz/solana.js'; * const [aggregatorAccount, aggregatorInit ] = await AggregatorAccount.createInstruction(program, payer, { * queueAccount, * queueAuthority, * batchSize: 5, * minRequiredOracleResults: 3, * minRequiredJobResults: 1, * minUpdateDelaySeconds: 30, * }); * const txnSignature = await program.signAndSend(aggregatorInit); * ``` */ public static async createInstruction( program: SwitchboardProgram, payer: PublicKey, params: AggregatorInitParams, options?: TransactionObjectOptions ): Promise<[AggregatorAccount, TransactionObject]> { if (params.batchSize > 8) { throw new errors.AggregatorConfigError( "oracleRequestBatchSize", "must be less than or equal to 8" ); } if (params.minUpdateDelaySeconds < 5) { throw new errors.AggregatorConfigError( "minUpdateDelaySeconds", "must be greater than 5 seconds" ); } const keypair = params.keypair ?? Keypair.generate(); program.verifyNewKeypair(keypair); const ixns: TransactionInstruction[] = []; const signers: Keypair[] = [keypair]; ixns.push( SystemProgram.createAccount({ fromPubkey: payer, newAccountPubkey: keypair.publicKey, space: program.account.aggregatorAccountData.size, lamports: await program.connection.getMinimumBalanceForRentExemption( program.account.aggregatorAccountData.size ), programId: program.programId, }) ); ixns.push( ix.aggregatorInit( program, { params: { name: [...Buffer.from(params.name ?? "", "utf8").slice(0, 32)], metadata: [ ...Buffer.from(params.metadata ?? "", "utf8").slice(0, 128), ], batchSize: params.batchSize, minOracleResults: params.minRequiredOracleResults, minJobResults: params.minRequiredJobResults, minUpdateDelaySeconds: params.minUpdateDelaySeconds, startAfter: new BN(params.startAfter ?? 0), varianceThreshold: SwitchboardDecimal.fromBig( new Big(params.varianceThreshold ?? 0) ).borsh, forceReportPeriod: new BN(params.forceReportPeriod ?? 0), expiration: new BN(params.expiration ?? 0), stateBump: program.programState.bump, disableCrank: params.disableCrank ?? false, }, }, { aggregator: keypair.publicKey, authority: params.authority ?? payer, queue: params.queueAccount.publicKey, programState: program.programState.publicKey, } ) ); const aggregatorInitTxn = new TransactionObject( payer, ixns, signers, options ); const aggregatorAccount = new AggregatorAccount(program, keypair.publicKey); return [aggregatorAccount, aggregatorInitTxn]; } /** * Creates an aggregator on-chain and return the transaction signature and created account resource. * @param program The SwitchboardProgram. * @param params aggregator configuration parameters. * @return Transaction signature and the newly created aggregatorAccount. * * Basic usage example: * * ```ts * import {AggregatorAccount} from '@switchboard-xyz/solana.js'; * const [aggregatorAccount, txnSignature] = await AggregatorAccount.create(program, { * queueAccount, * queueAuthority, * batchSize: 5, * minRequiredOracleResults: 3, * minRequiredJobResults: 1, * minUpdateDelaySeconds: 30, * }); * const aggregator = await aggregatorAccount.loadData(); * ``` */ public static async create( program: SwitchboardProgram, params: AggregatorInitParams, options?: SendTransactionObjectOptions ): Promise<[AggregatorAccount, TransactionSignature]> { const [account, transaction] = await AggregatorAccount.createInstruction( program, program.walletPubkey, params, options ); const txnSignature = await program.signAndSend(transaction, options); return [account, txnSignature]; } /** * Create the {@linkcode PermissionAccount} and {@linkcode LeaseAccount} for a new oracle queue without affecting the feed's rhythm. This does not evict a feed from the current queue. */ async transferQueuePart1Instructions( payer: PublicKey, params: { newQueue: QueueAccount; } & LeaseExtendParams ): Promise<[TransactionObject, PermissionAccount, LeaseAccount]> { const txns: Array = []; const aggregator = await this.loadData(); const newQueue = await params.newQueue.loadData(); const jobs = await this.loadJobs(aggregator); const jobAuthorities = jobs.map((job) => job.state.authority); const [newPermissionAccount] = this.getPermissionAccount( params.newQueue.publicKey, newQueue.authority ); const newPermissionAccountInfo = await this.program.connection.getAccountInfo( newPermissionAccount.publicKey ); if (newPermissionAccountInfo === null) { const [_newPermissionAccount, permissionInitTxn] = PermissionAccount.createInstruction(this.program, payer, { authority: newQueue.authority, granter: params.newQueue.publicKey, grantee: this.publicKey, }); assert( newPermissionAccount.publicKey.equals(_newPermissionAccount.publicKey) ); txns.push(permissionInitTxn); } const [newLeaseAccount] = this.getLeaseAccount(params.newQueue.publicKey); const newLeaseAccountInfo = await this.program.connection.getAccountInfo( newLeaseAccount.publicKey ); if (newLeaseAccountInfo === null) { const [userTokenWallet, userWrap] = params.fundAmount && params.fundAmount > 0 && !params.funderTokenWallet ? await this.program.mint.getOrCreateWrappedUserInstructions(payer, { fundUpTo: params.fundAmount ?? 0, }) : [undefined, undefined]; if (userWrap) { txns.push(userWrap); } // create lease for the new queue but dont transfer any balance const [_newLeaseAccount, leaseInit] = await LeaseAccount.createInstructions(this.program, payer, { aggregatorAccount: this, queueAccount: params.newQueue, funderTokenWallet: params.funderTokenWallet ?? userTokenWallet ?? undefined, funderAuthority: params.funderAuthority ?? undefined, jobAuthorities, fundAmount: params.fundAmount ?? 0, withdrawAuthority: aggregator.authority, // set to current authority jobPubkeys: aggregator.jobPubkeysData.slice( 0, aggregator.jobPubkeysSize ), }); assert(newLeaseAccount.publicKey.equals(_newLeaseAccount.publicKey)); txns.push(leaseInit); } const packed = TransactionObject.pack(txns); if (packed.length !== 1) { throw new Error( `QueueTransfer-Part1: Expected a single TransactionObject` ); } return [packed[0], newPermissionAccount, newLeaseAccount]; } /** * Create the {@linkcode PermissionAccount} and {@linkcode LeaseAccount} for a new oracle queue without affecting the feed's rhythm. This does not evict a feed from the current queue. */ async transferQueuePart1( params: { newQueue: QueueAccount; } & LeaseExtendParams ): Promise<[PermissionAccount, LeaseAccount, TransactionSignature]> { const [txn, permissionAccount, leaseAccount] = await this.transferQueuePart1Instructions( this.program.walletPubkey, params ); const signature = await this.program.signAndSend(txn); return [permissionAccount, leaseAccount, signature]; } /** * Approve the feed to use the new queue. Must be signed by the {@linkcode QueueAccount} authority. * * This does not affect the feed's rhythm nor evict it from the current queue. The Aggregator authority is still required to sign a transaction to move the feed. */ async transferQueuePart2Instructions( payer: PublicKey, params: { newQueue: QueueAccount; enable: boolean; queueAuthority?: Keypair; // dont check if new accounts have been created yet force?: boolean; } ): Promise<[TransactionObject | undefined, PermissionAccount]> { const newQueue = await params.newQueue.loadData(); const [permissionAccount] = this.getPermissionAccount( params.newQueue.publicKey, newQueue.authority ); if (!params.enable) { return [undefined, permissionAccount]; } if ( params.queueAuthority && !params.queueAuthority.publicKey.equals(newQueue.authority) ) { throw new errors.IncorrectAuthority( newQueue.authority, params.queueAuthority.publicKey ); } if (!params.force) { await permissionAccount.loadData().catch(() => { throw new Error(`Expected permissionAccount to be created already`); }); } const permissionSet = permissionAccount.setInstruction(payer, { enable: params.enable, queueAuthority: params.queueAuthority, permission: new PermitOracleQueueUsage(), }); return [permissionSet, permissionAccount]; } /** * Approve the feed to use the new queue. Must be signed by the {@linkcode QueueAccount} authority. * * This does not affect the feed's rhythm nor evict it from the current queue. The Aggregator authority is still required to sign a transaction to move the feed. */ async transferQueuePart2(params: { newQueue: QueueAccount; enable: boolean; queueAuthority?: Keypair; // dont check if new accounts have been created yet force?: boolean; }): Promise<[PermissionAccount, TransactionSignature | undefined]> { const [txn, permissionAccount] = await this.transferQueuePart2Instructions( this.program.walletPubkey, params ); if (!txn) { return [permissionAccount, undefined]; } const signature = await this.program.signAndSend(txn); return [permissionAccount, signature]; } /** * Transfer the feed to the new {@linkcode QueueAccount} and optionally push it on a crank. Must be signed by the Aggregator authority to approve the transfer. * * This will evict a feed from the previous queue and crank. */ async transferQueuePart3Instructions( payer: PublicKey, params: { newQueue: QueueAccount; authority?: Keypair; newCrank?: CrankAccount; // dont check if new accounts have been created yet force?: boolean; } ): Promise> { const txns: Array = []; const newQueue = await params.newQueue.loadData(); const aggregator = await this.loadData(); // new permission account needs to be created and approved const [permissionAccount] = this.getPermissionAccount( params.newQueue.publicKey, newQueue.authority ); if (!params.force) { await permissionAccount .loadData() .catch(() => { throw new Error(`Expected permissionAccount to be created already`); }) .then((permission) => { if ( !newQueue.unpermissionedFeedsEnabled && permission.permissions !== 2 ) { throw new Error( `PermissionAccount missing required permissions to enable this queue` ); } }); } // check the new lease has been created const [newLeaseAccount] = this.getLeaseAccount(params.newQueue.publicKey); if (!params.force) { await newLeaseAccount.loadData().catch(() => { throw new Error(`Expected leaseAccount to be created already`); }); } // set the feeds queue const setQueueTxn = new TransactionObject( payer, [ ix.aggregatorSetQueue( this.program, { params: {} }, { aggregator: this.publicKey, authority: aggregator.authority, queue: params.newQueue.publicKey, } ), ], params.authority ? [params.authority] : [] ); txns.push(setQueueTxn); // push to crank if (params.newCrank) { const newCrank = await params.newCrank.loadData(); if (!newCrank.queuePubkey.equals(params.newQueue.publicKey)) { throw new Error(`Crank is owned by the wrong queue`); } const crankPush = params.newCrank.pushInstructionSync(payer, { aggregatorAccount: this, queue: newQueue, crank: newCrank, }); txns.push(crankPush); } // remove any funds from the old lease account const [oldLeaseAccount] = this.getLeaseAccount(aggregator.queuePubkey); const oldLease = await oldLeaseAccount.loadData(); const oldLeaseBalance = await oldLeaseAccount.fetchBalance(oldLease.escrow); if (oldLease.withdrawAuthority.equals(payer) && oldLeaseBalance > 0) { const withdrawTxn = await oldLeaseAccount.withdrawInstruction(payer, { amount: oldLeaseBalance, unwrap: false, // withdraw old lease funds into the new lease withdrawWallet: this.program.mint.getAssociatedAddress( newLeaseAccount.publicKey ), }); txns.push(withdrawTxn); } return TransactionObject.pack(txns); } /** * Transfer the feed to the new {@linkcode QueueAccount} and optionally push it on a crank. Must be signed by the Aggregator authority to approve the transfer. * * This will evict a feed from the previous queue and crank. */ async transferQueuePart3(params: { newQueue: QueueAccount; authority?: Keypair; newCrank?: CrankAccount; // dont check if new accounts have been created yet force?: boolean; }): Promise> { const txns = await this.transferQueuePart3Instructions( this.program.walletPubkey, params ); const signatures = await this.program.signAndSendAll(txns, { skipPreflight: true, }); return signatures; } async transferQueueInstructions( payer: PublicKey, params: { authority?: Keypair; newQueue: QueueAccount; newCrank?: CrankAccount; enable: boolean; queueAuthority?: Keypair; } & LeaseExtendParams, opts?: TransactionObjectOptions ): Promise<[Array, PermissionAccount, LeaseAccount]> { const txns: Array = []; const [part1, permissionAccount, leaseAccount] = await this.transferQueuePart1Instructions(payer, { newQueue: params.newQueue, fundAmount: params.fundAmount, funderTokenWallet: params.funderTokenWallet, funderAuthority: params.funderAuthority, }); txns.push(part1); const [part2] = await this.transferQueuePart2Instructions(payer, { newQueue: params.newQueue, enable: params.enable, queueAuthority: params.queueAuthority, force: true, }); if (part2) { txns.push(part2); } const part3 = await this.transferQueuePart3Instructions(payer, { newQueue: params.newQueue, authority: params.authority, newCrank: params.newCrank, force: true, }); txns.push(...part3); return [ TransactionObject.pack(txns, opts), permissionAccount, leaseAccount, ]; } async transferQueue( params: { authority?: Keypair; newQueue: QueueAccount; newCrank?: CrankAccount; enable: boolean; queueAuthority?: Keypair; } & LeaseExtendParams ): Promise<[PermissionAccount, LeaseAccount, Array]> { const [txns, permissionAccount, leaseAccount] = await this.transferQueueInstructions(this.program.walletPubkey, params); const signatures = await this.program.signAndSendAll(txns, { skipPreflight: true, }); return [permissionAccount, leaseAccount, signatures]; } public getPermissionAccount( queuePubkey: PublicKey, queueAuthority: PublicKey ): [PermissionAccount, number] { return PermissionAccount.fromSeed( this.program, queueAuthority, queuePubkey, this.publicKey ); } public getLeaseAccount( queuePubkey: PublicKey ): [LeaseAccount, PublicKey, number] { const [leaseAccount, leaseBump] = LeaseAccount.fromSeed( this.program, queuePubkey, this.publicKey ); const leaseEscrow = this.program.mint.getAssociatedAddress( leaseAccount.publicKey ); return [leaseAccount, leaseEscrow, leaseBump]; } /** * Derives the Program Derived Accounts (PDAs) for the Aggregator, based on its currently assigned oracle queue. * * @param queueAccount The QueueAccount associated with the Aggregator. * @param queueAuthority The PublicKey of the oracle queue authority. * * @return An object containing the Aggregator PDA accounts, including: * - permissionAccount: The permission account. * - permissionBump: The nonce value used to generate the permission account. * - leaseAccount: The lease account. * - leaseBump: The nonce value used to generate the lease account. * - leaseEscrow: The lease escrow account. * * Basic usage example: * * ```ts * const aggregatorPdaAccounts = aggregator.getAccounts(queueAccount, queueAuthority); * console.log("Aggregator PDA accounts:", aggregatorPdaAccounts); * ``` */ public getAccounts( queueAccount: QueueAccount, queueAuthority: PublicKey ): AggregatorPdaAccounts { const [permissionAccount, permissionBump] = this.getPermissionAccount( queueAccount.publicKey, queueAuthority ); const [leaseAccount, leaseEscrow, leaseBump] = this.getLeaseAccount( queueAccount.publicKey ); return { permissionAccount, permissionBump, leaseAccount, leaseBump, leaseEscrow, }; } /** * Retrieves the latest confirmed value stored in the aggregator account from a pre-fetched account state. * * @param aggregator The pre-fetched aggregator account data. * * @return The latest feed value as a Big instance, or null if no successful rounds have been confirmed yet. * * Basic usage example: * * ```ts * const latestValue = AggregatorAccount.decodeLatestValue(aggregatorAccountData); * console.log("Latest confirmed value:", latestValue?.toString() ?? "No successful rounds yet"); * ``` */ public static decodeLatestValue( aggregator: AggregatorAccountData ): Big | null { if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) { return null; } const result = aggregator.latestConfirmedRound.result.toBig(); return result; } /** * Retrieves the latest confirmed value stored in the aggregator account. * * @return A Promise that resolves to the latest feed value as a Big instance, or null if the value is not populated or no successful rounds have been confirmed yet. * * Basic usage example: * * ```ts * const latestValue = await aggregatorAccount.fetchLatestValue(); * console.log("Latest confirmed value:", latestValue?.toString() ?? "No successful rounds yet"); * ``` */ public async fetchLatestValue(): Promise { const aggregator = await this.loadData(); return AggregatorAccount.decodeLatestValue(aggregator); } /** * Retrieves the timestamp of the latest confirmed round stored in the aggregator account from a pre-fetched account state. * * @param aggregator The pre-fetched aggregator account data. * * @return The latest feed timestamp as an BN instance. * * @throws Error if the aggregator currently holds no value or no successful rounds have been confirmed yet. * * Basic usage example: * * ```ts * const latestTimestamp = AggregatorAccount.decodeLatestTimestamp(aggregatorAccountData); * console.log("Latest confirmed round timestamp:", latestTimestamp.toString()); * ``` */ public static decodeLatestTimestamp(aggregator: AggregatorAccountData): BN { if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) { throw new Error("Aggregator currently holds no value."); } return aggregator.latestConfirmedRound.roundOpenTimestamp; } /** * Decodes the confirmed round results of the aggregator account from a pre-fetched account state. * * @param aggregator The pre-fetched aggregator account data. * * @return An array of objects containing the oracle public keys and their respective reported values as Big instances. * * @throws Error if the aggregator currently holds no value or no successful rounds have been confirmed yet. * * Basic usage example: * * ```ts * const confirmedRoundResults = AggregatorAccount.decodeConfirmedRoundResults(aggregatorAccountData); * console.log("Confirmed round results:", confirmedRoundResults); * ``` */ public static decodeConfirmedRoundResults( aggregator: AggregatorAccountData ): Array<{ oraclePubkeys: PublicKey; value: Big }> { if ((aggregator.latestConfirmedRound?.numSuccess ?? 0) === 0) { throw new Error("Aggregator currently holds no value."); } const results: Array<{ oraclePubkeys: PublicKey; value: Big }> = []; for (let i = 0; i < aggregator.oracleRequestBatchSize; ++i) { if (aggregator.latestConfirmedRound.mediansFulfilled[i] === true) { results.push({ oraclePubkeys: aggregator.latestConfirmedRound.oraclePubkeysData[i], value: aggregator.latestConfirmedRound.mediansData[i].toBig(), }); } } return results; } /** * Retrieves the individual oracle results of the latest confirmed round from a pre-fetched account state. * * @param aggregator The pre-fetched aggregator account data. * * @return An array of objects containing the oracle account instances and their respective reported values as Big instances. * * Basic usage example: * * ```ts * const aggregatorAccountData = await aggregatorAccount.loadData(); * const confirmedRoundResults = aggregatorAccount.getConfirmedRoundResults(aggregatorAccountData); * console.log("Confirmed round results by oracle account:", confirmedRoundResults); * ``` */ public getConfirmedRoundResults( aggregator: AggregatorAccountData ): Array<{ oracleAccount: OracleAccount; value: Big }> { return AggregatorAccount.decodeConfirmedRoundResults(aggregator).map( (o) => { return { oracleAccount: new OracleAccount(this.program, o.oraclePubkeys), value: o.value, }; } ); } /** * Generates a SHA-256 hash of all the oracle jobs currently in the aggregator. * * @param jobs An array of OracleJob instances representing the jobs in the aggregator. * * @return A crypto.Hash object representing the hash of all the feed jobs. * * Basic usage example: * * ```ts * const jobs = [job1, job2, job3]; * const jobsHash = aggregatorAccount.produceJobsHash(jobs); * console.log("Hash of all the feed jobs:", jobsHash); * ``` */ public produceJobsHash(jobs: Array): 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; } public static decodeCurrentRoundOracles( aggregator: AggregatorAccountData ): Array { return aggregator.currentRound.oraclePubkeysData.slice( 0, aggregator.oracleRequestBatchSize ); } public async loadCurrentRoundOracles( aggregator: AggregatorAccountData ): Promise> { return await Promise.all( AggregatorAccount.decodeCurrentRoundOracles(aggregator).map(async (o) => { const oracleAccount = new OracleAccount(this.program, o); return { account: oracleAccount, state: await oracleAccount.loadData(), }; }) ); } public static decodeJobPubkeys( aggregator: AggregatorAccountData ): Array { return aggregator.jobPubkeysData.slice(0, aggregator.jobPubkeysSize); } public async loadJobs(aggregator: AggregatorAccountData): Promise< Array<{ account: JobAccount; state: JobAccountData; job: OracleJob; weight: number; }> > { const jobAccountDatas = await anchor.utils.rpc.getMultipleAccounts( this.program.connection, AggregatorAccount.decodeJobPubkeys(aggregator) ); return await Promise.all( jobAccountDatas.map(async (j) => { if (!j?.account) { throw new Error( `Failed to fetch account data for job ${j?.publicKey}` ); } if (!j.account.owner.equals(this.program.programId)) { throw new errors.IncorrectOwner( this.program.programId, j.account.owner ); } const jobAccount = new JobAccount(this.program, j.publicKey); const jobState: JobAccountData = this.program.coder.decode( "JobAccountData", j.account.data ); return { account: jobAccount, state: jobState, job: OracleJob.decodeDelimited(jobState.data), weight: Math.max( aggregator.jobWeights[ aggregator.jobPubkeysData.findIndex((x: PublicKey) => x.equals(j.publicKey) ) ] ?? 1, 1 ), }; }) ); } public getJobHashes( jobs: Array<{ account: JobAccount; state: JobAccountData; }> ): Array { return jobs.map((j) => Buffer.from(new Uint8Array(j.state.hash))); } /** * Validates an aggregator's configuration. * * @param aggregator An object containing the aggregator's account data or a partial configuration. * @param queue An object containing the OracleQueueAccountData. * @param target An object containing the target configuration values to be verified. * * @throws {AggregatorConfigError} If any of the following conditions are met: * - minUpdateDelaySeconds is less than 5 seconds * - batchSize is greater than 8 * - batchSize is greater than the queue size * - minOracleResults is greater than batchSize * - minJobResults is equal to 0 * * Basic usage example: * * ```ts * AggregatorAccount.verifyConfig( * aggregatorData, * queueData, * { * batchSize: 8, * minOracleResults: 5, * minJobResults: 4, * minUpdateDelaySeconds: 10, * } * ); * ``` */ public static verifyConfig( aggregator: | AggregatorAccountData | { oracleRequestBatchSize: number; minOracleResults: number; minJobResults: number; minUpdateDelaySeconds: number; jobPubkeysSize: number; }, queue: OracleQueueAccountData, target: { batchSize?: number; minOracleResults?: number; minJobResults?: number; minUpdateDelaySeconds?: number; } ): void { const numberOfOracles = queue.size; const endState = { batchSize: target.batchSize ?? aggregator.oracleRequestBatchSize, minOracleResults: target.minOracleResults ?? aggregator.minOracleResults, minJobResults: target.minJobResults ?? aggregator.minJobResults, minUpdateDelaySeconds: target.minUpdateDelaySeconds ?? aggregator.minUpdateDelaySeconds, }; if (endState.batchSize > 8) { throw new errors.AggregatorConfigError( "oracleRequestBatchSize", "must be less than or equal to 8" ); } if (endState.minUpdateDelaySeconds < 5) { throw new errors.AggregatorConfigError( "minUpdateDelaySeconds", "must be greater than 5 seconds" ); } if (endState.minJobResults === 0 || endState.minJobResults > 16) { throw new errors.AggregatorConfigError( "minJobResults", "must be a value between 1 and 16" ); } if (endState.minJobResults > aggregator.jobPubkeysSize) { console.warn( `The aggregator's minJobResults (${endState.minJobResults}) is less than the current number of jobs (${aggregator.jobPubkeysSize})` ); } if (endState.batchSize > numberOfOracles) { throw new errors.AggregatorConfigError( "oracleRequestBatchSize", `must be less than the number of oracles actively heartbeating on the queue (${numberOfOracles})` ); } if (endState.minOracleResults > endState.batchSize) { throw new errors.AggregatorConfigError( "minOracleResults", `must be less than the oracleRequestBatchSize (${endState.batchSize})` ); } } /** * Validates an aggregator's configuration. * * @param aggregator An object containing the aggregator's account data or a partial configuration. * @param queue An object containing the OracleQueueAccountData. * @param target An object containing the target configuration values to be verified. * * @throws {AggregatorConfigError} If any of the following conditions are met: * - minUpdateDelaySeconds is less than 5 seconds * - batchSize is greater than 8 * - batchSize is greater than the queue size * - minOracleResults is greater than batchSize * - minJobResults is greater than the aggregator's jobPubkeysSize * * Basic usage example: * * ```ts * aggregatorAccount.verifyConfig( * aggregatorData, * queueData, * { * batchSize: 8, * minOracleResults: 5, * minJobResults: 4, * minUpdateDelaySeconds: 10, * } * ); * ``` */ public verifyConfig( aggregator: | AggregatorAccountData | { oracleRequestBatchSize: number; minOracleResults: number; minJobResults: number; minUpdateDelaySeconds: number; jobPubkeysSize: number; }, queue: OracleQueueAccountData, target: { batchSize?: number; minOracleResults?: number; minJobResults?: number; minUpdateDelaySeconds?: number; } ): void { AggregatorAccount.verifyConfig(aggregator, queue, target); } /** * Creates a transaction object to set aggregator configuration parameters. * * @param payer The public key of the payer account. * @param params An object containing partial configuration parameters to be set. * @param options Optional transaction object options. * * @return A promise that resolves to a transaction object containing the setConfig instruction. * * Basic usage example: * * ```ts * const transactionObject = await aggregatorAccount.setConfigInstruction( * payerPublicKey, * { * name: 'New Aggregator Name', * metadata: 'New Aggregator Metadata', * batchSize: 8, * minOracleResults: 5, * minJobResults: 4, * minUpdateDelaySeconds: 10, * forceReportPeriod: 20, * varianceThreshold: 0.01, * basePriorityFee: 1, * priorityFeeBump: 0.1, * priorityFeeBumpPeriod: 60, * maxPriorityFeeMultiplier: 5, * force: false, * } * ); * ``` */ public async setConfigInstruction( payer: PublicKey, params: Partial<{ name: string; metadata: string; batchSize: number; minOracleResults: number; minJobResults: number; minUpdateDelaySeconds: number; forceReportPeriod: number; varianceThreshold: number; authority: Keypair; basePriorityFee: number; priorityFeeBump: number; priorityFeeBumpPeriod: number; maxPriorityFeeMultiplier: number; force: boolean; disableCrank: boolean; }>, options?: TransactionObjectOptions ): Promise { if (!(params.force ?? false)) { const aggregator = await this.loadData(); const queueAccount = new QueueAccount( this.program, aggregator.queuePubkey ); const queue = await queueAccount.loadData(); this.verifyConfig(aggregator, queue, { batchSize: params.batchSize, minOracleResults: params.minOracleResults, minJobResults: params.minJobResults, minUpdateDelaySeconds: params.minUpdateDelaySeconds, }); } const varianceThreshold = params.varianceThreshold ?? 0; const setConfigIxn = ix.aggregatorSetConfig( this.program, { params: { name: params.name ? ([ ...Buffer.from(params.name, "utf-8").slice(0, 32), ] as Array) : null, metadata: params.metadata ? ([ ...Buffer.from(params.metadata, "utf-8").slice(0, 128), ] as Array) : null, batchSize: params.batchSize ?? null, minOracleResults: params.minOracleResults ?? null, minUpdateDelaySeconds: params.minUpdateDelaySeconds ?? null, minJobResults: params.minJobResults ?? null, forceReportPeriod: params.forceReportPeriod ?? null, varianceThreshold: varianceThreshold >= 0 ? new BorshDecimal( SwitchboardDecimal.fromBig(new Big(varianceThreshold)) ) : null, basePriorityFee: params.basePriorityFee ?? null, priorityFeeBump: params.priorityFeeBump ?? null, priorityFeeBumpPeriod: params.priorityFeeBumpPeriod ?? null, maxPriorityFeeMultiplier: params.maxPriorityFeeMultiplier ?? null, disableCrank: params.disableCrank ?? null, }, }, { aggregator: this.publicKey, authority: params.authority ? params.authority.publicKey : payer, } ); return new TransactionObject( payer, [setConfigIxn], params.authority ? [params.authority] : [], options ); } /** * Sets an aggregator configuration parameters. * * @param params An object containing partial configuration parameters to be set. * @param options Optional transaction object options. * * @return A promise that resolves to a transaction object containing the setConfig instruction. * * Basic usage example: * * ```ts * const transactionObject = await aggregatorAccount.setConfig( * { * name: 'New Aggregator Name', * metadata: 'New Aggregator Metadata', * batchSize: 8, * minOracleResults: 5, * minJobResults: 4, * minUpdateDelaySeconds: 10, * forceReportPeriod: 20, * varianceThreshold: 0.01, * basePriorityFee: 1, * priorityFeeBump: 0.1, * priorityFeeBumpPeriod: 60, * maxPriorityFeeMultiplier: 5, * force: false, * } * ); * ``` */ public async setConfig( params: Partial<{ name: string; metadata: string; batchSize: number; minOracleResults: number; minJobResults: number; minUpdateDelaySeconds: number; forceReportPeriod: number; varianceThreshold: number; authority?: Keypair; basePriorityFee?: number; priorityFeeBump?: number; priorityFeeBumpPeriod?: number; maxPriorityFeeMultiplier?: number; force: boolean; disableCrank: boolean; }>, options?: SendTransactionObjectOptions ): Promise { const setConfigTxn = await this.setConfigInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(setConfigTxn); return txnSignature; } public setQueueInstruction( payer: PublicKey, params: { queueAccount: QueueAccount; authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { const setQueueIxn = ix.aggregatorSetQueue( this.program, { params: {}, }, { aggregator: this.publicKey, authority: params.authority ? params.authority.publicKey : payer, queue: params.queueAccount.publicKey, } ); return new TransactionObject( payer, [setQueueIxn], params.authority ? [params.authority] : [], options ); } public async setQueue( params: { queueAccount: QueueAccount; authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const setQueueTxn = this.setQueueInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(setQueueTxn, options); return txnSignature; } public addJobInstruction( payer: PublicKey, params: { job: JobAccount; weight?: number; authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { const authority = params.authority ? params.authority.publicKey : payer; const addJobIxn = ix.aggregatorAddJob( this.program, { params: { weight: params.weight ?? 1 } }, { aggregator: this.publicKey, authority: authority, job: params.job.publicKey, } ); return new TransactionObject( payer, [addJobIxn], params.authority ? [params.authority] : [], options ); } public async addJob( params: { job: JobAccount; weight?: number; authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const txn = this.addJobInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(txn, options); return txnSignature; } public lockInstruction( payer: PublicKey, params: { authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { return new TransactionObject( payer, [ ix.aggregatorLock( this.program, { params: {} }, { aggregator: this.publicKey, authority: params.authority ? params.authority.publicKey : payer, } ), ], params.authority ? [params.authority] : [], options ); } public async lock( params: { authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const lockTxn = this.lockInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(lockTxn, options); return txnSignature; } public setAuthorityInstruction( payer: PublicKey, params: { newAuthority: PublicKey; authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { return new TransactionObject( payer, [ ix.aggregatorSetAuthority( this.program, { params: {} }, { aggregator: this.publicKey, authority: params.authority ? params.authority.publicKey : payer, newAuthority: params.newAuthority, } ), ], params.authority ? [params.authority] : [], options ); } public async setAuthority( params: { newAuthority: PublicKey; authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const setAuthorityTxn = this.setAuthorityInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend( setAuthorityTxn, options ); return txnSignature; } public updateJobWeightInstruction( payer: PublicKey, params: { job: JobAccount; jobIdx: number; weight: number; authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { const removeJob = this.removeJobInstruction( payer, { job: params.job, jobIdx: params.jobIdx, authority: params.authority, }, options ); const addJob = this.addJobInstruction(payer, { job: params.job, weight: params.weight, authority: params.authority, }); return removeJob.combine(addJob); } public async updateJobWeight( params: { job: JobAccount; jobIdx: number; weight: number; authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const transaction = this.updateJobWeightInstruction( this.program.walletPubkey, params, options ); const signature = await this.program.signAndSend(transaction, options); return signature; } public removeJobInstruction( payer: PublicKey, params: { job: JobAccount; jobIdx: number; authority?: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { const authority = params.authority ? params.authority.publicKey : payer; const removeJobIxn = ix.aggregatorRemoveJob( this.program, { params: { jobIdx: params.jobIdx } }, { aggregator: this.publicKey, authority: authority, job: params.job.publicKey, } ); return new TransactionObject( payer, [removeJobIxn], params.authority ? [params.authority] : [], options ); } public async removeJob( params: { job: JobAccount; jobIdx: number; authority?: Keypair; }, options?: SendTransactionObjectOptions ): Promise { const removeJobTxn = this.removeJobInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(removeJobTxn, options); return txnSignature; } public async openRoundInstruction( payer: PublicKey, params?: { payoutWallet?: PublicKey }, options?: TransactionObjectOptions ): Promise { const aggregatorData = await this.loadData(); const queueAccount = new QueueAccount( this.program, aggregatorData.queuePubkey ); const queue = await queueAccount.loadData(); const { permissionAccount, permissionBump, leaseAccount, leaseBump, leaseEscrow, } = this.getAccounts(queueAccount, queue.authority); const ixns: Array = []; const payoutWallet = params?.payoutWallet ?? this.program.mint.getAssociatedAddress(payer); const payoutWalletAccountInfo = await this.program.connection.getAccountInfo(payoutWallet); if (payoutWalletAccountInfo === null) { const [createTokenAccountTxn] = this.program.mint.createAssocatedUserInstruction(payer); ixns.push(...createTokenAccountTxn.ixns); } ixns.push( ix.aggregatorOpenRound( this.program, { params: { stateBump: this.program.programState.bump, leaseBump, permissionBump, jitter: 0, }, }, { aggregator: this.publicKey, lease: leaseAccount.publicKey, oracleQueue: queueAccount.publicKey, queueAuthority: queue.authority, permission: permissionAccount.publicKey, escrow: leaseEscrow, programState: this.program.programState.publicKey, payoutWallet: payoutWallet, tokenProgram: TOKEN_PROGRAM_ID, dataBuffer: queue.dataBuffer, mint: this.program.mint.address, } ) ); return new TransactionObject(payer, ixns, [], options); } public async openRound( params?: { payoutWallet?: PublicKey; }, options?: SendTransactionObjectOptions ): Promise { const openRoundTxn = await this.openRoundInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(openRoundTxn, options); return txnSignature; } public quoteKeypairFromSeed(seed: PublicKey): Keypair { const hash = createHash("sha256"); hash.update(Buffer.from("QuoteAccountData")); hash.update(seed.toBuffer()); const kp = Keypair.fromSeed(hash.digest()); return kp; } public teeSaveResultInstructionSync( payer: PublicKey, params: AggregatorSaveResultSyncParams & { quotePubkey?: PublicKey; authority: Keypair; }, options?: TransactionObjectOptions ): TransactionObject { const [oraclePermissionAccount, oraclePermissionBump] = params.oraclePermission; const quote = params.quotePubkey ?? this.quoteKeypairFromSeed( params.oracles[params.oracleIdx].state.oracleAuthority ).publicKey; const saveResultIxn = ix.aggregatorTeeSaveResult( this.program, { params: { // oracleIdx: params.oracleIdx, // error: params.error ?? false, value: SwitchboardDecimal.fromBig(params.value).borsh, jobsChecksum: [...this.produceJobsHash(params.jobs).digest()], minResponse: SwitchboardDecimal.fromBig(params.minResponse).borsh, maxResponse: SwitchboardDecimal.fromBig(params.maxResponse).borsh, feedPermissionBump: params.permissionBump, oraclePermissionBump: oraclePermissionBump, leaseBump: params.leaseBump, stateBump: this.program.programState.bump, }, }, { aggregator: this.publicKey, oracle: params.oracles[params.oracleIdx].account.publicKey, oracleAuthority: params.oracles[params.oracleIdx].state.oracleAuthority, oracleQueue: params.queueAccount.publicKey, queueAuthority: params.queueAuthority, feedPermission: params.permissionAccount.publicKey, oraclePermission: oraclePermissionAccount.publicKey, lease: params.leaseAccount.publicKey, escrow: params.leaseEscrow, tokenProgram: spl.TOKEN_PROGRAM_ID, programState: this.program.programState.publicKey, historyBuffer: params.historyBuffer ?? this.publicKey, mint: this.program.mint.address, slider: this.slidingWindowKey, quote: quote, rewardWallet: this.program.mint.getAssociatedAddress(payer), payer: payer, systemProgram: SystemProgram.programId, } ); const remainingAccounts: Array = []; params.oracles.forEach((oracle) => remainingAccounts.push(oracle.account.publicKey) ); params.oracles.forEach((oracle) => remainingAccounts.push(oracle.state.tokenAccount) ); remainingAccounts.push(this.slidingWindowKey); saveResultIxn.keys.push( ...remainingAccounts.map((pubkey): AccountMeta => { return { isSigner: false, isWritable: true, pubkey }; }) ); return new TransactionObject( payer, [saveResultIxn], [params.authority], options ); } public saveResultInstructionSync( payer: PublicKey, params: AggregatorSaveResultSyncParams, options?: TransactionObjectOptions ): TransactionObject { const [oraclePermissionAccount, oraclePermissionBump] = params.oraclePermission; if (params.oracleIdx < 0 || params.oracleIdx > params.oracles.length - 1) { throw new Error("Failed to find oracle in current round"); } const saveResultIxn = ix.aggregatorSaveResult( this.program, { params: { oracleIdx: params.oracleIdx, error: params.error ?? false, value: SwitchboardDecimal.fromBig(params.value).borsh, jobsChecksum: [...this.produceJobsHash(params.jobs).digest()], minResponse: SwitchboardDecimal.fromBig(params.minResponse).borsh, maxResponse: SwitchboardDecimal.fromBig(params.maxResponse).borsh, feedPermissionBump: params.permissionBump, oraclePermissionBump: oraclePermissionBump, leaseBump: params.leaseBump, stateBump: this.program.programState.bump, }, }, { aggregator: this.publicKey, oracle: params.oracles[params.oracleIdx].account.publicKey, oracleAuthority: params.oracles[params.oracleIdx].state.oracleAuthority, oracleQueue: params.queueAccount.publicKey, queueAuthority: params.queueAuthority, feedPermission: params.permissionAccount.publicKey, oraclePermission: oraclePermissionAccount.publicKey, lease: params.leaseAccount.publicKey, escrow: params.leaseEscrow, tokenProgram: spl.TOKEN_PROGRAM_ID, programState: this.program.programState.publicKey, historyBuffer: params.historyBuffer ?? this.publicKey, mint: this.program.mint.address, } ); const remainingAccounts: Array = []; params.oracles.forEach((oracle) => remainingAccounts.push(oracle.account.publicKey) ); params.oracles.forEach((oracle) => remainingAccounts.push(oracle.state.tokenAccount) ); remainingAccounts.push(this.slidingWindowKey); saveResultIxn.keys.push( ...remainingAccounts.map((pubkey): AccountMeta => { return { isSigner: false, isWritable: true, pubkey }; }) ); return new TransactionObject(payer, [saveResultIxn], [], options); } public async saveResultInstruction( payer: PublicKey, params: AggregatorSaveResultAsyncParams, options?: TransactionObjectOptions ): Promise { const aggregator = params.aggregator ?? (await this.loadData()); const oracles = params.oracles ?? (await this.loadCurrentRoundOracles(aggregator)); const oracleIdx = params.oracleIdx ?? oracles.findIndex((o) => o.account.publicKey.equals(params.oracleAccount.publicKey) ); if (oracleIdx < 0 || oracleIdx > oracles.length - 1) { throw new Error("Failed to find oracle in current round"); } const queueAccount = params.queueAccount ?? new QueueAccount(this.program, aggregator.queuePubkey); const queueAuthority = params.queueAuthority ?? (await queueAccount.loadData()).authority; const [oraclePermissionAccount, oraclePermissionBump] = params.oraclePermission ?? params.oracleAccount.getPermissionAccount( queueAccount.publicKey, queueAuthority ); const accounts: AggregatorPdaAccounts = params.permissionAccount === undefined || params.leaseAccount === undefined || params.leaseEscrow === undefined || params.permissionBump === undefined || params.leaseBump === undefined ? this.getAccounts(queueAccount, queueAuthority) : { permissionAccount: params.permissionAccount, permissionBump: params.permissionBump, leaseAccount: params.leaseAccount, leaseBump: params.leaseBump, leaseEscrow: params.leaseEscrow, }; const saveResultTxn = this.saveResultInstructionSync( payer, { ...accounts, queueAccount, queueAuthority, jobs: params.jobs, historyBuffer: aggregator.historyBuffer.equals(PublicKey.default) ? undefined : aggregator.historyBuffer, oracleIdx, oraclePermission: [oraclePermissionAccount, oraclePermissionBump], value: params.value, minResponse: params.minResponse, maxResponse: params.maxResponse, error: params.error ?? false, aggregator: aggregator, oracles: oracles, }, options ); return saveResultTxn; } public async saveResult( params: AggregatorSaveResultAsyncParams, options?: SendTransactionObjectOptions ): Promise { const saveResultTxn = await this.saveResultInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(saveResultTxn, options); return txnSignature; } public async fetchAccounts( _aggregator?: AggregatorAccountData, _queueAccount?: QueueAccount, _queue?: OracleQueueAccountData, commitment: Commitment = "confirmed" ): Promise { const aggregator = _aggregator ?? (await this.loadData()); const queueAccount = _queueAccount ?? new QueueAccount(this.program, aggregator.queuePubkey); const queue = _queue ?? (await queueAccount.loadData()); const { permissionAccount, permissionBump, leaseAccount, leaseEscrow, leaseBump, } = this.getAccounts(queueAccount, queue.authority); const jobPubkeys = aggregator.jobPubkeysData.slice( 0, aggregator.jobPubkeysSize ); const accountInfos = await anchor.utils.rpc.getMultipleAccounts( this.program.connection, [ permissionAccount.publicKey, leaseAccount.publicKey, leaseEscrow, ...jobPubkeys, ], commitment ); const permissionAccountInfo = accountInfos.shift(); if (!permissionAccountInfo || !permissionAccountInfo.account) { throw new Error( `PermissionAccount has not been created yet for this aggregator` ); } const permission = PermissionAccountData.decode( permissionAccountInfo.account.data ); const leaseAccountInfo = accountInfos.shift(); if (!leaseAccountInfo || !leaseAccountInfo.account) { throw new Error( `LeaseAccount has not been created yet for this aggregator` ); } const lease = LeaseAccountData.decode(leaseAccountInfo.account.data); const leaseEscrowAccountInfo = accountInfos.shift(); if (!leaseEscrowAccountInfo || !leaseEscrowAccountInfo.account) { throw new Error( `LeaseAccount escrow has not been created yet for this aggregator` ); } const leaseEscrowAccount = spl.unpackAccount( leaseEscrow, leaseEscrowAccountInfo.account ); const jobs: Array<{ publicKey: PublicKey; data: JobAccountData; tasks: Array; }> = []; accountInfos.map((accountInfo) => { if (!accountInfo || !accountInfo.account) { throw new Error(`Failed to fetch JobAccount`); } const job = JobAccountData.decode(accountInfo.account.data); const oracleJob = OracleJob.decodeDelimited(job.data); jobs.push({ publicKey: accountInfo.publicKey, data: job, tasks: oracleJob.tasks, }); }); return { aggregator: { publicKey: this.publicKey, data: aggregator, }, queue: { publicKey: queueAccount.publicKey, data: queue, }, permission: { publicKey: permissionAccount.publicKey, bump: permissionBump, data: permission, }, lease: { publicKey: leaseAccount.publicKey, bump: leaseBump, data: lease, balance: this.program.mint.fromTokenAmount(leaseEscrowAccount.amount), }, jobs: jobs, }; } public async toAccountsJSON( _aggregator?: AggregatorAccountData, _queueAccount?: QueueAccount, _queue?: OracleQueueAccountData ): Promise { const accounts = await this.fetchAccounts( _aggregator, _queueAccount, _queue ); return { publicKey: this.publicKey, ...accounts.aggregator.data.toJSON(), queue: { publicKey: accounts.queue.publicKey, ...accounts.queue.data.toJSON(), }, permission: { publicKey: accounts.permission.publicKey, ...accounts.permission.data.toJSON(), bump: accounts.permission.bump, }, lease: { publicKey: accounts.lease.publicKey, ...accounts.lease.data.toJSON(), bump: accounts.lease.bump, balance: accounts.lease.balance, }, jobs: accounts.jobs.map((j) => { return { publicKey: j.publicKey, ...j.data.toJSON(), tasks: j.tasks, }; }), }; } setSlidingWindowInstruction( payer: PublicKey, params: { authority?: Keypair; mode: AggregatorResolutionModeKind; }, options?: TransactionObjectOptions ): TransactionObject { return new TransactionObject( payer, [ ix.aggregatorSetResolutionMode( this.program, { params: { mode: params.mode.discriminator }, }, { aggregator: this.publicKey, authority: params.authority ? params.authority.publicKey : payer, slidingWindow: this.slidingWindowKey, payer: payer, systemProgram: SystemProgram.programId, } ), ], params.authority ? [params.authority] : [], options ); } async setSlidingWindow( params: { authority?: Keypair; mode: AggregatorResolutionModeKind; }, options?: TransactionObjectOptions ): Promise { const setSlidingWindowTxn = this.setSlidingWindowInstruction( this.program.walletPubkey, params, options ); const txnSignature = await this.program.signAndSend(setSlidingWindowTxn); return txnSignature; } async openRoundAndAwaitResult( params?: { payoutWallet?: PublicKey } & { aggregator?: AggregatorAccountData; }, timeout = 30000, options?: TransactionObjectOptions ): Promise<[AggregatorAccountData, TransactionSignature | undefined]> { const aggregator = params?.aggregator ?? (await this.loadData()); const currentRoundOpenSlot = aggregator.latestConfirmedRound.roundOpenSlot; let ws: number | undefined = undefined; const closeWebsocket = async () => { if (ws !== undefined) { await this.program.connection.removeAccountChangeListener(ws); ws = undefined; } }; const statePromise: Promise = promiseWithTimeout( timeout, new Promise((resolve: (result: AggregatorAccountData) => void) => { ws = this.onChange((aggregator) => { // if confirmed round slot larger than last open slot // AND sliding window mode or sufficient oracle results if ( aggregator.latestConfirmedRound.roundOpenSlot.gt( currentRoundOpenSlot ) && (aggregator.resolutionMode.kind === AggregatorResolutionMode.ModeSlidingResolution.kind || (aggregator.latestConfirmedRound.numSuccess ?? 0) >= aggregator.minOracleResults) ) { resolve(aggregator); } }); }) ).finally(async () => { await closeWebsocket(); }); const openRoundSignature = await this.openRound(params, options).catch( async (error) => { await closeWebsocket(); throw new Error(`Failed to call openRound, ${error}`); } ); const state = await statePromise; await closeWebsocket(); return [state, openRoundSignature]; } /** * Await for the next round to close and return the aggregator round result * * @param roundOpenSlot - optional, the slot when the current round was opened. if not provided then it will be loaded. * @param timeout - the number of milliseconds to wait for the round to close * * @throws {string} when the timeout interval is exceeded or when the latestConfirmedRound.roundOpenSlot exceeds the target roundOpenSlot */ async nextRound( roundOpenSlot?: BN, timeout = 30000 ): Promise { const slot = roundOpenSlot ?? (await this.loadData()).currentRound.roundOpenSlot; let ws: number | undefined; let result: AggregatorAccountData; try { result = await promiseWithTimeout( timeout, new Promise((resolve: (result: AggregatorAccountData) => void) => { ws = this.onChange((aggregator) => { if (aggregator.latestConfirmedRound.roundOpenSlot.eq(slot)) { resolve(aggregator); } }); }) ); } finally { if (ws) { await this.program.connection.removeAccountChangeListener(ws); } } return result; } /** * Load an aggregators {@linkcode AggregatorHistoryBuffer}. * @return the list of historical samples attached to the aggregator. */ async loadHistory( startTimestamp?: number, endTimestamp?: number ): Promise> { if (!this.history) { this.history = new AggregatorHistoryBuffer( this.program, (await this.loadData()).historyBuffer ); } const history = await this.history.loadData(startTimestamp, endTimestamp); return history; } static async fetchMultiple( program: SwitchboardProgram, publicKeys: Array, commitment: Commitment = "confirmed" ): Promise< Array<{ account: AggregatorAccount; data: AggregatorAccountData; }> > { const aggregators: Array<{ account: AggregatorAccount; data: AggregatorAccountData; }> = []; const accountInfos = await anchor.utils.rpc.getMultipleAccounts( program.connection, publicKeys, commitment ); for (const accountInfo of accountInfos) { if (!accountInfo?.publicKey) { continue; } try { const account = new AggregatorAccount(program, accountInfo.publicKey); const data = AggregatorAccountData.decode(accountInfo.account.data); aggregators.push({ account, data }); // eslint-disable-next-line no-empty } catch {} } return aggregators; } /** * Calculates the required priority fee for a given aggregator * * Multiplier = the minimum of maxPriorityFeeMultiplier and (timestamp - lastUpdatedTimestamp) / priorityFeeBumpPeriod * Fee = baseFee + basePriorityFee + priorityFeeBump * multiplier * * @param aggregator - the current aggregator state including its last updated timestamp and priority fee config * @param timestamp - optional, the current unix timestamp. can provide the SolanaClock timestamp for better accuracy * @param baseFee - optional, the Solana compute unit base fee * * @returns the solana priority fee to include in the save_result action */ public static calculatePriorityFee( aggregator: AggregatorAccountData, timestamp = Math.round(Date.now() / 1000), baseFee = 0 // base compute unit price ): number { // parse defaults const lastUpdateTimestamp = aggregator.latestConfirmedRound.roundOpenTimestamp.gt(new BN(0)) ? aggregator.latestConfirmedRound.roundOpenTimestamp.toNumber() : timestamp; // on first update this would cause max multiplier const priorityFeeBumpPeriod = Math.max(1, aggregator.priorityFeeBumpPeriod); // cant divide by 0 const maxPriorityFeeMultiplier = Math.max( 1, aggregator.maxPriorityFeeMultiplier ); // calculate staleness multiplier const multiplier = Math.min( (timestamp - lastUpdateTimestamp) / priorityFeeBumpPeriod, maxPriorityFeeMultiplier ); const feeBump = aggregator.priorityFeeBump * multiplier; const fee = baseFee + aggregator.basePriorityFee + feeBump; if (Number.isNaN(fee)) { return 0; } // Should we enforce some upper limit? Like 1 SOL? // Probably not, gives MEV bots a floor return Math.round(fee); } /** Fetch the balance of an aggregator's lease */ public async fetchBalance( leaseEscrow?: PublicKey, queuePubkey?: PublicKey ): Promise { const escrowPubkey = leaseEscrow ?? this.getLeaseAccount( queuePubkey ?? (await this.loadData()).queuePubkey )[1]; const escrowBalance = await this.program.mint.fetchBalance(escrowPubkey); if (escrowBalance === null) { throw new errors.AccountNotFoundError("Lease Escrow", escrowPubkey); } return escrowBalance; } /** Create a transaction object that will fund an aggregator's lease up to a given balance */ public async fundUpToInstruction( payer: PublicKey, fundUpTo: number, disableWrap = false ): Promise<[TransactionObject | undefined, number | undefined]> { const [leaseAccount, leaseEscrow] = this.getLeaseAccount( (await this.loadData()).queuePubkey ); const balance = await this.fetchBalance(leaseEscrow); if (balance >= fundUpTo) { return [undefined, undefined]; } const fundAmount = fundUpTo - balance; const leaseExtend = await leaseAccount.extendInstruction(payer, { fundAmount, disableWrap, }); return [leaseExtend, fundAmount]; } /** Fund an aggregator's lease up to a given balance */ public async fundUpTo( payer: PublicKey, fundUpTo: number, disableWrap = false ): Promise<[TransactionSignature | undefined, number | undefined]> { const [fundUpToTxn, fundAmount] = await this.fundUpToInstruction( payer, fundUpTo, disableWrap ); if (!fundUpToTxn) { return [undefined, undefined]; } const txnSignature = await this.program.signAndSend(fundUpToTxn); return [txnSignature, fundAmount!]; } /** Create a set of transactions that will fund an aggregator's lease up to a given balance */ public static async fundMultipleUpToInstructions( payer: PublicKey, fundUpTo: number, aggregators: Array, options?: TransactionPackOptions | undefined ): Promise> { if (aggregators.length === 0) { throw new Error(`No aggregator accounts provided`); } const program = aggregators[0].program; const txns: Array = []; let wrapAmount = 0; for (const aggregator of aggregators) { const [depositTxn, depositAmount] = await aggregator.fundUpToInstruction( payer, fundUpTo, true ); if (depositTxn && depositAmount) { txns.push(depositTxn); wrapAmount = wrapAmount + depositAmount; } } const [_, wrapTxn] = await program.mint.getOrCreateWrappedUserInstructions( payer, { fundUpTo: wrapAmount, } ); if (wrapTxn) { txns.unshift(wrapTxn); } return TransactionObject.pack(txns, options); } /** Fund multiple aggregator account lease's up to a given balance */ public static async fundMultipleUpTo( fundUpTo: number, aggregators: Array, options?: TransactionPackOptions | undefined ): Promise> { if (aggregators.length === 0) { throw new Error(`No aggregator accounts provided`); } const program = aggregators[0].program; const txns = await AggregatorAccount.fundMultipleUpToInstructions( program.walletPubkey, fundUpTo, aggregators, options ); const txnSignatures = await program.signAndSendAll(txns); return txnSignatures; } public async closeInstructions( payer: PublicKey, params?: { authority?: Keypair; tokenWallet?: PublicKey; }, opts?: TransactionObjectOptions ): Promise { if (this.program.cluster === "mainnet-beta") { throw new Error( `Aggregators can only be closed with the devnet version of Switchboard` ); } const [tokenWallet, tokenWalletInit] = params?.tokenWallet ? [params.tokenWallet, undefined] : await this.program.mint.getOrCreateWrappedUserInstructions(payer, { fundUpTo: 0, }); const aggregator = await this.loadData(); const [queueAccount, queue] = await QueueAccount.load( this.program, aggregator.queuePubkey ); const slidingWindowKey = this.slidingWindowKey; const slidingWindowAccountInfo = await this.program.connection.getAccountInfo(slidingWindowKey); const { permissionAccount, permissionBump, leaseAccount, leaseBump, leaseEscrow, } = this.getAccounts(queueAccount, queue.authority); const crankPubkey: PublicKey | null = aggregator.crankPubkey.equals( PublicKey.default ) ? null : aggregator.crankPubkey; let dataBuffer: PublicKey | null = null; if (crankPubkey !== null) { const [crankAccount, crank] = await CrankAccount.load( this.program, crankPubkey ); dataBuffer = crank.dataBuffer; } const accounts = { authority: aggregator.authority, aggregator: this.publicKey, permission: permissionAccount.publicKey, lease: leaseAccount.publicKey, escrow: leaseEscrow, oracleQueue: queueAccount.publicKey, queueAuthority: queue.authority, programState: queueAccount.program.programState.publicKey, solDest: payer, escrowDest: tokenWallet, tokenProgram: TOKEN_PROGRAM_ID, }; if (crankPubkey !== null && dataBuffer !== null) { accounts["crank"] = crankPubkey; accounts["dataBuffer"] = dataBuffer; } else { accounts["crank"] = null; accounts["dataBuffer"] = null; } if (slidingWindowAccountInfo !== null) { accounts["slidingWindow"] = slidingWindowKey; } else { accounts["slidingWindow"] = null; } const closeIxn = await ( (this.program as any)._program as anchor.Program ).methods .aggregatorClose({ stateBump: this.program.programState.bump, permissionBump: permissionBump, leaseBump: leaseBump, }) .accounts( accounts as any // compiler expects all types to be pubkeys ) .instruction(); const closeTxn = tokenWalletInit ? tokenWalletInit.add( closeIxn, params?.authority ? [params.authority] : undefined ) : new TransactionObject( payer, [closeIxn], params?.authority ? [params.authority] : [] ); // add txn options return new TransactionObject( closeTxn.payer, closeTxn.ixns, closeTxn.signers, opts ); } public async close( params?: { authority?: Keypair; tokenWallet?: PublicKey; }, opts?: TransactionObjectOptions ): Promise { const closeTxn = await this.closeInstructions( this.program.walletPubkey, params, opts ); const txnSignature = await this.program.signAndSend(closeTxn); return txnSignature; } } /** * Parameters to initialize an aggregator account. */ export interface AggregatorInitParams { /** * Name of the aggregator to store on-chain. */ name?: string; /** * Metadata of the aggregator to store on-chain. */ metadata?: string; /** * 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; /** * 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?: number; /** * unix_timestamp after which funds may be withdrawn from the aggregator. * null/undefined/0 means the feed has no expiration. */ expiration?: number; /** * If true, this aggregator is disallowed from being updated by a crank on the queue. */ disableCrank?: boolean; /** * Optional pre-existing keypair to use for aggregator initialization. */ keypair?: Keypair; /** * If included, this keypair will be the aggregator authority rather than * the aggregator keypair. */ authority?: PublicKey; /** * The queue to which this aggregator will be linked */ queueAccount: QueueAccount; /** * The authority of the queue. */ queueAuthority: PublicKey; } export interface AggregatorSetQueueParams { queueAccount: QueueAccount; authority?: Keypair; } /** * Parameters required to open an aggregator round */ export interface AggregatorOpenRoundParams { /** * The oracle queue from which oracles are assigned this update. */ oracleQueueAccount: QueueAccount; /** * The token wallet which will receive rewards for calling update on this feed. */ payoutWallet: PublicKey; } /** * 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 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; /** * Authority of the queue the aggregator is attached to. */ queueAuthority: PublicKey; /** * Program token mint. */ tokenMint: PublicKey; /** * List of parsed oracles. */ oracles: Array; } export type AggregatorAccountsJSON = AggregatorAccountDataJSON & { publicKey: PublicKey; queue: OracleQueueAccountDataJSON & { publicKey: PublicKey }; permission: PermissionAccountDataJSON & { bump: number; publicKey: PublicKey; }; lease: LeaseAccountDataJSON & { bump: number; publicKey: PublicKey } & { balance: number; }; jobs: Array< JobAccountDataJSON & { publicKey: PublicKey; tasks: Array; } >; }; export type AggregatorAccounts = { aggregator: { publicKey: PublicKey; data: AggregatorAccountData; }; queue: { publicKey: PublicKey; data: OracleQueueAccountData; }; permission: { publicKey: PublicKey; bump: number; data: PermissionAccountData; }; lease: { publicKey: PublicKey; bump: number; balance: number; data: LeaseAccountData; }; jobs: Array<{ publicKey: PublicKey; data: JobAccountData; tasks: Array; }>; }; export type AggregatorPdaAccounts = { permissionAccount: PermissionAccount; permissionBump: number; leaseAccount: LeaseAccount; leaseBump: number; leaseEscrow: PublicKey; }; export type SaveResultResponse = { jobs: Array; // oracleAccount: OracleAccount; value: Big; minResponse: Big; maxResponse: Big; error?: boolean; }; export type SaveResultAccounts = AggregatorPdaAccounts & { aggregator: AggregatorAccountData; // queue queueAccount: QueueAccount; queueAuthority: PublicKey; // oracle oraclePermission: [PermissionAccount, number]; oracles: Array<{ account: OracleAccount; state: OracleAccountData }>; oracleIdx: number; // history historyBuffer?: PublicKey; }; export type AggregatorSaveResultSyncParams = SaveResultResponse & SaveResultAccounts; export type AggregatorSaveResultAsyncParams = SaveResultResponse & (Partial & { oracleAccount: OracleAccount });