sbv2-solana/javascript/solana.js/src/accounts/aggregatorAccount.ts

2778 lines
82 KiB
TypeScript

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<AggregatorAccountData> {
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<AggregatorAccountData>(
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<AggregatorAccountData>,
options?: {
lamports?: number;
rentEpoch?: number;
}
): AccountInfo<Buffer> {
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<AggregatorAccountData>,
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<AggregatorAccountData> {
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<TransactionObject> = [];
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<Array<TransactionObject>> {
const txns: Array<TransactionObject> = [];
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<Array<TransactionSignature>> {
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<TransactionObject>, PermissionAccount, LeaseAccount]> {
const txns: Array<TransactionObject> = [];
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<TransactionSignature>]> {
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<Big | null> {
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<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;
}
public static decodeCurrentRoundOracles(
aggregator: AggregatorAccountData
): Array<PublicKey> {
return aggregator.currentRound.oraclePubkeysData.slice(
0,
aggregator.oracleRequestBatchSize
);
}
public async loadCurrentRoundOracles(
aggregator: AggregatorAccountData
): Promise<Array<{ account: OracleAccount; state: OracleAccountData }>> {
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<PublicKey> {
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<Buffer> {
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<TransactionObject> {
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<number>)
: null,
metadata: params.metadata
? ([
...Buffer.from(params.metadata, "utf-8").slice(0, 128),
] as Array<number>)
: 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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionSignature> {
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<TransactionObject> {
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<TransactionInstruction> = [];
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<TransactionSignature> {
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<PublicKey> = [];
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<PublicKey> = [];
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<TransactionObject> {
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<TransactionSignature> {
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<AggregatorAccounts> {
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<OracleJob.ITask>;
}> = [];
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<AggregatorAccountsJSON> {
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<TransactionSignature> {
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<AggregatorAccountData> = 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<AggregatorAccountData> {
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<Array<AggregatorHistoryRow>> {
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<PublicKey>,
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<number> {
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<AggregatorAccount>,
options?: TransactionPackOptions | undefined
): Promise<Array<TransactionObject>> {
if (aggregators.length === 0) {
throw new Error(`No aggregator accounts provided`);
}
const program = aggregators[0].program;
const txns: Array<TransactionObject> = [];
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<AggregatorAccount>,
options?: TransactionPackOptions | undefined
): Promise<Array<TransactionSignature>> {
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<TransactionObject> {
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<TransactionSignature> {
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<OracleJob>;
/**
* Authority of the queue the aggregator is attached to.
*/
queueAuthority: PublicKey;
/**
* Program token mint.
*/
tokenMint: PublicKey;
/**
* List of parsed oracles.
*/
oracles: Array<OracleAccountData>;
}
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<OracleJob.ITask>;
}
>;
};
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<OracleJob.ITask>;
}>;
};
export type AggregatorPdaAccounts = {
permissionAccount: PermissionAccount;
permissionBump: number;
leaseAccount: LeaseAccount;
leaseBump: number;
leaseEscrow: PublicKey;
};
export type SaveResultResponse = {
jobs: Array<OracleJob>;
// 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<SaveResultAccounts> & { oracleAccount: OracleAccount });