diff --git a/javascript/solana.js/generate-client.js b/javascript/solana.js/generate-client.js index 6b7a62c..3e07cdc 100755 --- a/javascript/solana.js/generate-client.js +++ b/javascript/solana.js/generate-client.js @@ -60,7 +60,7 @@ async function main() { ); execSync( - 'rm -rf ./src/generated && npx anchor-client-gen --program-id SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f ./src/idl/mainnet.json ./src/generated' + 'rm -rf ./src/generated && npx anchor-client-gen --program-id SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f ../../../switchboard-core/switchboard_v2/target/idl/switchboard_v2.json ./src/generated' ); fs.writeFileSync( './src/generated/index.ts', diff --git a/javascript/solana.js/src/accounts/aggregatorAccount.ts b/javascript/solana.js/src/accounts/aggregatorAccount.ts index 2f65468..93fd29b 100644 --- a/javascript/solana.js/src/accounts/aggregatorAccount.ts +++ b/javascript/solana.js/src/accounts/aggregatorAccount.ts @@ -24,6 +24,7 @@ import { PermissionAccount } from './permissionAccount'; import * as spl from '@solana/spl-token'; import { TransactionObject } from '../transaction'; import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { AggregatorHistoryBuffer } from './aggregatorHistoryBuffer'; /** * Account type holding a data feed's update configuration, job accounts, and its current result. @@ -40,6 +41,8 @@ import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; export class AggregatorAccount extends Account { static accountName = 'AggregatorAccountData'; + public history?: AggregatorHistoryBuffer; + /** * Returns the aggregator's name buffer in a stringified format. */ @@ -96,6 +99,7 @@ export class AggregatorAccount extends Account { this.publicKey ); if (data === null) throw new errors.AccountNotFoundError(this.publicKey); + this.history = AggregatorHistoryBuffer.fromAggregator(this.program, data); return data; } @@ -224,63 +228,6 @@ export class AggregatorAccount extends Account { return [txnSignature, account]; } - /** - * Decode an aggregators history buffer and return an array of historical samples - * @params historyBuffer the historyBuffer AccountInfo stored on-chain - * @return the array of {@linkcode types.AggregatorHistoryRow} samples - */ - public static decodeHistory( - bufferAccountInfo: AccountInfo - ): Array { - const historyBuffer = bufferAccountInfo.data ?? Buffer.from(''); - const ROW_SIZE = 28; - - if (historyBuffer.length < 12) { - return []; - } - - const insertIdx = historyBuffer.readUInt32LE(8) * ROW_SIZE; - const front: Array = []; - const tail: Array = []; - for (let i = 12; i < historyBuffer.length; i += ROW_SIZE) { - if (i + ROW_SIZE > historyBuffer.length) { - break; - } - const row = types.AggregatorHistoryRow.fromDecoded( - types.AggregatorHistoryRow.layout().decode(historyBuffer, i) - ); - if (row.timestamp.eq(new anchor.BN(0))) { - break; - } - if (i <= insertIdx) { - tail.push(row); - } else { - front.push(row); - } - } - return front.concat(tail); - } - - /** - * Fetch an aggregators history buffer and return an array of historical samples - * @params aggregator the pre-loaded aggregator state - * @return the array of {@linkcode types.AggregatorHistoryRow} samples - */ - public async loadHistory( - aggregator: types.AggregatorAccountData - ): Promise> { - if (PublicKey.default.equals(aggregator.historyBuffer)) { - return []; - } - const bufferAccountInfo = await this.program.connection.getAccountInfo( - aggregator.historyBuffer - ); - if (bufferAccountInfo === null) { - throw new errors.AccountNotFoundError(aggregator.historyBuffer); - } - return AggregatorAccount.decodeHistory(bufferAccountInfo); - } - public getAccounts(params: { queueAccount: QueueAccount; queueAuthority: PublicKey; @@ -570,62 +517,6 @@ export class AggregatorAccount extends Account { return txnSignature; } - static getHistoryBufferSize(samples: number): number { - return 8 + 4 + samples * 28; - } - - public async setHistoryBufferInstruction( - payer: PublicKey, - params: { - size: number; - authority?: Keypair; - buffer?: Keypair; - } - ): Promise { - const buffer = params.buffer ?? Keypair.generate(); - const ixns: TransactionInstruction[] = []; - const signers: Keypair[] = params.authority - ? [params.authority, buffer] - : [buffer]; - - const size = AggregatorAccount.getHistoryBufferSize(params.size); - - ixns.push( - SystemProgram.createAccount({ - fromPubkey: payer, - newAccountPubkey: buffer.publicKey, - space: size, - lamports: - await this.program.connection.getMinimumBalanceForRentExemption(size), - programId: this.program.programId, - }), - types.aggregatorSetHistoryBuffer( - this.program, - { params: {} }, - { - aggregator: this.publicKey, - authority: params.authority ? params.authority.publicKey : payer, - buffer: buffer.publicKey, - } - ) - ); - - return new TransactionObject(payer, ixns, signers); - } - - public async setHistoryBuffer(params: { - size: number; - authority?: Keypair; - buffer?: Keypair; - }): Promise { - const setHistoryTxn = await this.setHistoryBufferInstruction( - this.program.walletPubkey, - params - ); - const txnSignature = await this.program.signAndSend(setHistoryTxn); - return txnSignature; - } - public setQueueInstruction( payer: PublicKey, params: { @@ -1282,13 +1173,40 @@ export interface AggregatorInitParams { } export type AggregatorSetConfigParams = Partial<{ - name: Buffer; - metadata: Buffer; + /** + * 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. + */ minOracleResults: number; + /** + * Minimum number of feed jobs suggested to be successful before an oracle + * sends a response. + */ minJobResults: number; + /** + * Minimum number of seconds required between aggregator rounds. + */ minUpdateDelaySeconds: number; + /** + * Number of seconds for which, even if the variance threshold is not passed, + * accept new responses from oracles. + */ forceReportPeriod: number; + /** + * Change percentage required between a previous round and the current round. + * If variance percentage is not met, reject new oracle responses. + */ varianceThreshold: number; }>; @@ -1315,11 +1233,11 @@ export interface AggregatorOpenRoundParams { * 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; diff --git a/javascript/solana.js/src/accounts/aggregatorHistoryBuffer.ts b/javascript/solana.js/src/accounts/aggregatorHistoryBuffer.ts index ab90c09..d9612b8 100644 --- a/javascript/solana.js/src/accounts/aggregatorHistoryBuffer.ts +++ b/javascript/solana.js/src/accounts/aggregatorHistoryBuffer.ts @@ -80,7 +80,7 @@ export class AggregatorHistoryBuffer extends Account< program: SwitchboardProgram, aggregator: types.AggregatorAccountData ): AggregatorHistoryBuffer | undefined { - if (aggregator.historyBuffer.equals(aggregator.historyBuffer)) { + if (aggregator.historyBuffer.equals(PublicKey.default)) { return undefined; } diff --git a/javascript/solana.js/src/accounts/crankAccount.ts b/javascript/solana.js/src/accounts/crankAccount.ts index 05384e6..f34e46c 100644 --- a/javascript/solana.js/src/accounts/crankAccount.ts +++ b/javascript/solana.js/src/accounts/crankAccount.ts @@ -15,6 +15,7 @@ import { SwitchboardProgram } from '../program'; import { TransactionObject } from '../transaction'; import { Account, OnAccountChangeCallback } from './account'; import { AggregatorAccount } from './aggregatorAccount'; +import { CrankDataBuffer } from './crankDataBuffer'; import { QueueAccount } from './queueAccount'; /** @@ -27,7 +28,7 @@ export class CrankAccount extends Account { static accountName = 'CrankAccountData'; /** The public key of the crank's data buffer storing a priority queue of {@linkcode AggregatorAccount}'s and their next available update timestamp */ - dataBuffer?: PublicKey; + dataBuffer?: CrankDataBuffer; /** * Get the size of an {@linkcode CrankAccount} on-chain. @@ -51,30 +52,6 @@ export class CrankAccount extends Account { ); } - /** - * Invoke a callback each time a crank's buffer has changed on-chain. The buffer stores a list of {@linkcode AggregatorAccount} public keys along with their next available update time. - * @param callback - the callback invoked when the crank's buffer changes - * @param commitment - optional, the desired transaction finality. defaults to 'confirmed' - * @returns the websocket subscription id - */ - onBufferChange( - callback: OnAccountChangeCallback>, - _dataBuffer?: PublicKey, - commitment: Commitment = 'confirmed' - ): number { - const buffer = this.dataBuffer ?? _dataBuffer; - if (!buffer) { - throw new Error( - `No crank dataBuffer provided. Call crankAccount.loadData() or pass it to this function in order to watch the account for changes` - ); - } - return this.program.connection.onAccountChange( - buffer, - accountInfo => callback(CrankAccount.decodeBuffer(accountInfo)), - commitment - ); - } - /** * Retrieve and decode the {@linkcode types.CrankAccountData} stored in this account. */ @@ -84,59 +61,10 @@ export class CrankAccount extends Account { this.publicKey ); if (data === null) throw new errors.AccountNotFoundError(this.publicKey); - this.dataBuffer = data.dataBuffer; + this.dataBuffer = CrankDataBuffer.fromCrank(this.program, data); return data; } - public static decodeBuffer( - bufferAccountInfo: AccountInfo - ): Array { - const buffer = bufferAccountInfo.data.slice(8) ?? Buffer.from(''); - const maxRows = Math.floor(buffer.byteLength / 40); - - const pqData: Array = []; - - for (let i = 0; i < maxRows * 40; i += 40) { - if (buffer.byteLength - i < 40) { - break; - } - - const rowBuf = buffer.slice(i, i + 40); - const pubkey = new PublicKey(rowBuf.slice(0, 32)); - if (pubkey.equals(PublicKey.default)) { - break; - } - - const nextTimestamp = new anchor.BN(rowBuf.slice(32, 40), 'le'); - pqData.push(new types.CrankRow({ pubkey, nextTimestamp })); - } - - return pqData; - } - - public async loadCrank( - sorted = false, - commitment: Commitment = 'confirmed' - ): Promise> { - // Can we do this in a single RPC call? Do we need pqSize? - const dataBuffer = this.dataBuffer ?? (await this.loadData()).dataBuffer; - const bufferAccountInfo = await this.program.connection.getAccountInfo( - dataBuffer, - { commitment } - ); - if (bufferAccountInfo === null) { - throw new errors.AccountNotFoundError(dataBuffer); - } - - const pqData = CrankAccount.decodeBuffer(bufferAccountInfo); - - if (sorted) { - return pqData.sort((a, b) => a.nextTimestamp.cmp(b.nextTimestamp)); - } - - return pqData; - } - public static async createInstructions( program: SwitchboardProgram, payer: PublicKey, @@ -319,7 +247,8 @@ export class CrankAccount extends Account { lease: leaseAccount.publicKey, escrow: leaseEscrow, programState: this.program.programState.publicKey, - dataBuffer: this.dataBuffer ?? (await this.loadData()).dataBuffer, + dataBuffer: + this.dataBuffer?.publicKey ?? (await this.loadData()).dataBuffer, } ), ], @@ -347,7 +276,15 @@ export class CrankAccount extends Account { * @return List of {@linkcode types.CrankRow}, ordered by timestamp. */ async peakNextWithTime(num?: number): Promise { - const crankRows = await this.loadCrank(true); + let crank: CrankDataBuffer; + if (this.dataBuffer) { + crank = this.dataBuffer; + } else { + const crankData = await this.loadData(); + crank = new CrankDataBuffer(this.program, crankData.dataBuffer); + } + const crankRows = await crank.loadData(); + return crankRows.slice(0, num ?? crankRows.length); } @@ -380,7 +317,14 @@ export class CrankAccount extends Account { pubkey: PublicKey, crankRows?: Array ): Promise { - const rows = crankRows ?? (await this.loadCrank()); + let crank: CrankDataBuffer; + if (this.dataBuffer) { + crank = this.dataBuffer; + } else { + const crankData = await this.loadData(); + crank = new CrankDataBuffer(this.program, crankData.dataBuffer); + } + const rows = await crank.loadData(); const idx = rows.findIndex(r => r.pubkey.equals(pubkey)); if (idx === -1) { diff --git a/javascript/solana.js/src/accounts/crankDataBuffer.ts b/javascript/solana.js/src/accounts/crankDataBuffer.ts new file mode 100644 index 0000000..ad6b195 --- /dev/null +++ b/javascript/solana.js/src/accounts/crankDataBuffer.ts @@ -0,0 +1,99 @@ +import * as anchor from '@project-serum/anchor'; +import { AccountInfo, Commitment, PublicKey } from '@solana/web3.js'; +import * as errors from '../errors'; +import * as types from '../generated'; +import { SwitchboardProgram } from '../program'; +import { Account, OnAccountChangeCallback } from './account'; + +/** + * Account holding a priority queue of aggregators and their next available update time. + * + * Data: Array<{@linkcode types.CrankRow}> + */ +export class CrankDataBuffer extends Account> { + static accountName = 'CrankDataBuffer'; + + public size = 0; + + /** + * Invoke a callback each time a crank's buffer has changed on-chain. The buffer stores a list of {@linkcode AggregatorAccount} public keys along with their next available update time. + * @param callback - the callback invoked when the crank's buffer changes + * @param commitment - optional, the desired transaction finality. defaults to 'confirmed' + * @returns the websocket subscription id + */ + onChange( + callback: OnAccountChangeCallback>, + commitment: Commitment = 'confirmed' + ): number { + if (this.publicKey.equals(PublicKey.default)) { + throw new Error( + `No crank dataBuffer provided. Call crankAccount.loadData() or pass it to this function in order to watch the account for changes` + ); + } + return this.program.connection.onAccountChange( + this.publicKey, + accountInfo => callback(CrankDataBuffer.decode(accountInfo)), + commitment + ); + } + + /** + * Retrieve and decode the {@linkcode types.CrankAccountData} stored in this account. + */ + public async loadData(): Promise> { + if (this.publicKey.equals(PublicKey.default)) { + return []; + } + const accountInfo = await this.program.connection.getAccountInfo( + this.publicKey + ); + if (accountInfo === null) + throw new errors.AccountNotFoundError(this.publicKey); + const data = CrankDataBuffer.decode(accountInfo); + return data; + } + + public static decode( + bufferAccountInfo: AccountInfo + ): Array { + const buffer = bufferAccountInfo.data.slice(8) ?? Buffer.from(''); + const maxRows = Math.floor(buffer.byteLength / 40); + + const pqData: Array = []; + + for (let i = 0; i < maxRows * 40; i += 40) { + if (buffer.byteLength - i < 40) { + break; + } + + const rowBuf = buffer.slice(i, i + 40); + const pubkey = new PublicKey(rowBuf.slice(0, 32)); + if (pubkey.equals(PublicKey.default)) { + break; + } + + const nextTimestamp = new anchor.BN(rowBuf.slice(32, 40), 'le'); + pqData.push(new types.CrankRow({ pubkey, nextTimestamp })); + } + + return pqData; + } + + static getDataBufferSize(size: number): number { + return 8 + size * 40; + } + + /** + * Return an aggregator's assigned history buffer or undefined if it doesn't exist. + */ + static fromCrank( + program: SwitchboardProgram, + crank: types.CrankAccountData + ): CrankDataBuffer { + if (crank.dataBuffer.equals(PublicKey.default)) { + throw new Error(`Failed to find crank data buffer`); + } + + return new CrankDataBuffer(program, crank.dataBuffer); + } +} diff --git a/javascript/solana.js/src/accounts/index.ts b/javascript/solana.js/src/accounts/index.ts index 246a049..f6d0163 100644 --- a/javascript/solana.js/src/accounts/index.ts +++ b/javascript/solana.js/src/accounts/index.ts @@ -1,7 +1,9 @@ export * from './account'; export * from './aggregatorAccount'; +export * from './aggregatorHistoryBuffer'; export * from './bufferRelayAccount'; export * from './crankAccount'; +export * from './crankDataBuffer'; export * from './jobAccount'; export * from './leaseAccount'; export * from './oracleAccount'; diff --git a/javascript/solana.js/src/accounts/queueAccount.ts b/javascript/solana.js/src/accounts/queueAccount.ts index 8a20cc6..cc8bc96 100644 --- a/javascript/solana.js/src/accounts/queueAccount.ts +++ b/javascript/solana.js/src/accounts/queueAccount.ts @@ -20,6 +20,7 @@ import { SwitchboardProgram } from '../program'; import { TransactionObject } from '../transaction'; import { Account, OnAccountChangeCallback } from './account'; import { AggregatorAccount, AggregatorInitParams } from './aggregatorAccount'; +import { AggregatorHistoryBuffer } from './aggregatorHistoryBuffer'; import { BufferRelayerAccount, BufferRelayerInit } from './bufferRelayAccount'; import { CrankAccount, CrankInitParams } from './crankAccount'; import { JobAccount, JobInitParams } from './jobAccount'; @@ -550,12 +551,12 @@ export class QueueAccount extends Account { } if (params.historyLimit && params.historyLimit > 0) { - post.push( - await aggregatorAccount.setHistoryBufferInstruction( - this.program.walletPubkey, - { size: params.historyLimit, authority: params.authority } - ) - ); + const [historyBufferInit, historyBuffer] = + await AggregatorHistoryBuffer.createInstructions(this.program, payer, { + aggregatorAccount, + maxSamples: params.historyLimit, + }); + post.push(historyBufferInit); } const packed = TransactionObject.pack([