solana.js: doc comments and misc cleanup

This commit is contained in:
Conner Gallagher 2022-11-30 23:19:53 -07:00
parent e5e641ed28
commit a76f7db7ac
15 changed files with 396 additions and 297 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ target
.crates
# Misc
.DS_STORE
.keypairs
secrets
*-keypair*.json

View File

@ -5,7 +5,6 @@ import * as errors from '../errors';
import Big from 'big.js';
import { SwitchboardProgram } from '../program';
import {
AccountInfo,
AccountMeta,
Commitment,
Keypair,
@ -30,6 +29,9 @@ import { AggregatorHistoryBuffer } from './aggregatorHistoryBuffer';
* Account type holding a data feed's update configuration, job accounts, and its current result.
*
* Data: {@linkcode types.AggregatorAccountData}
*
* Result: {@linkcode types.SwitchboardDecimal}
*
* HistoryBuffer?: Array<{@linkcode types.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.
@ -140,6 +142,7 @@ export class AggregatorAccount extends Account<types.AggregatorAccountData> {
params: AggregatorInitParams
): Promise<[TransactionObject, AggregatorAccount]> {
const keypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(keypair);
const ixns: TransactionInstruction[] = [];
const signers: Keypair[] = [keypair];

View File

@ -164,6 +164,7 @@ export class AggregatorHistoryBuffer extends Account<
params: AggregatorHistoryInit
): Promise<[TransactionObject, AggregatorHistoryBuffer]> {
const buffer = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(buffer);
const ixns: TransactionInstruction[] = [];
const signers: Keypair[] = params.aggregatorAuthority

View File

@ -94,6 +94,8 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
}
): Promise<[TransactionObject, BufferRelayerAccount]> {
const keypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(keypair);
const size = 2048;
const ixns: TransactionInstruction[] = [];

View File

@ -22,7 +22,8 @@ import { QueueAccount } from './queueAccount';
* Account holding a priority queue of aggregators and their next available update time. This is a scheduling mechanism to ensure {@linkcode AggregatorAccount}'s are updated as close as possible to their specified update interval.
*
* Data: {@linkcode types.CrankAccountData}
* Buffer: Array<{@linkcode types.CrankRow}>
*
* Buffer: {@linkcode CrankDataBuffer}
*/
export class CrankAccount extends Account<types.CrankAccountData> {
static accountName = 'CrankAccountData';
@ -61,7 +62,10 @@ export class CrankAccount extends Account<types.CrankAccountData> {
this.publicKey
);
if (data === null) throw new errors.AccountNotFoundError(this.publicKey);
this.dataBuffer = CrankDataBuffer.fromCrank(this.program, data);
if (!this.dataBuffer) {
this.dataBuffer = CrankDataBuffer.fromCrank(this.program, data);
}
return data;
}
@ -71,7 +75,11 @@ export class CrankAccount extends Account<types.CrankAccountData> {
params: CrankInitParams
): Promise<[TransactionObject, CrankAccount]> {
const crankAccount = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(crankAccount);
const buffer = anchor.web3.Keypair.generate();
program.verifyNewKeypair(buffer);
const maxRows = params.maxRows ?? 500;
const crankSize = maxRows * 40 + 8;
@ -312,19 +320,29 @@ export class CrankAccount extends Account<types.CrankAccountData> {
return crankRows.map(row => row.pubkey);
}
/**
* Load a cranks {@linkcode CrankDataBuffer}.
* @return the list of aggregtors and their next available update time.
*/
async loadCrank(): Promise<Array<types.CrankRow>> {
if (!this.dataBuffer) {
this.dataBuffer = new CrankDataBuffer(
this.program,
(await this.loadData()).dataBuffer
);
}
const crankRows = await this.dataBuffer.loadData();
return crankRows;
}
/** Whether an aggregator pubkey is active on a Crank */
async isOnCrank(
pubkey: PublicKey,
crankRows?: Array<types.CrankRow>
): Promise<boolean> {
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 rows = crankRows ?? (await this.loadCrank());
const idx = rows.findIndex(r => r.pubkey.equals(pubkey));
if (idx === -1) {

View File

@ -10,4 +10,5 @@ export * from './oracleAccount';
export * from './permissionAccount';
export * from './programStateAccount';
export * from './queueAccount';
export * from './queueDataBuffer';
export * from './vrfAccount';

View File

@ -57,6 +57,8 @@ export class JobAccount extends Account<types.JobAccountData> {
}
const jobKeypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(jobKeypair);
const authority = params.authority ?? payer;
const CHUNK_SIZE = 800;

View File

@ -213,7 +213,7 @@ export class OracleAccount extends Account<types.OracleAccountData> {
params.tokenWallet ?? (await this.loadData()).tokenAccount;
const queue = params.queue ?? (await params.queueAccount.loadData());
const oracles = await params.queueAccount.loadOracles(queue);
const oracles = await params.queueAccount.loadOracles();
let lastPubkey = this.publicKey;
if (queue.size !== 0) {

View File

@ -15,19 +15,14 @@ export interface PermissionAccountInitParams {
authority: PublicKey;
}
export type PermissionSetParams =
| {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: boolean;
/** Keypair used to enable heartbeat permissions if payer is not the queue authority. */
queueAuthority?: Keypair;
}
| {
/** Whether to enable PERMIT_ORACLE_HEARTBEAT permissions. **Note:** Requires a provided queueAuthority keypair or payer to be the assigned queue authority. */
enable?: boolean;
/** Keypair used to enable heartbeat permissions if payer is not the queue authority. */
queueAuthority?: Keypair;
};
export interface PermissionSetParams {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: types.SwitchboardPermissionKind;
/** Whether to enable PERMIT_ORACLE_HEARTBEAT permissions. **Note:** Requires a provided queueAuthority keypair or payer to be the assigned queue authority. */
enable: boolean;
/** Keypair used to enable heartbeat permissions if payer is not the queue authority. */
queueAuthority?: Keypair;
}
/**
* Account type dictating the level of permissions between a granter and a grantee.
@ -132,11 +127,7 @@ export class PermissionAccount extends Account<types.PermissionAccountData> {
/**
* Sets the permission in the PermissionAccount
*/
public async set(params: {
permission: types.SwitchboardPermissionKind;
enable: boolean;
authority?: Keypair;
}): Promise<string> {
public async set(params: PermissionSetParams): Promise<string> {
const setTxn = this.setInstruction(this.program.walletPubkey, params);
const txnSignature = await this.program.signAndSend(setTxn);
return txnSignature;
@ -147,25 +138,28 @@ export class PermissionAccount extends Account<types.PermissionAccountData> {
*/
public setInstruction(
payer: PublicKey,
params: {
permission: types.SwitchboardPermissionKind;
enable: boolean;
authority?: Keypair;
}
params: PermissionSetParams
): TransactionObject {
return new TransactionObject(
payer,
[
types.permissionSet(
this.program,
{ params },
{
params: {
permission: params.permission,
enable: params.enable,
},
},
{
permission: this.publicKey,
authority: params.authority ? params.authority.publicKey : payer,
authority: params.queueAuthority
? params.queueAuthority.publicKey
: payer,
}
),
],
params.authority ? [params.authority] : []
params.queueAuthority ? [params.queueAuthority] : []
);
}
}

View File

@ -19,6 +19,7 @@ import { TransactionObject } from '../transaction';
*/
export class ProgramStateAccount extends Account<types.SbState> {
static accountName = 'SbState';
/**
* Retrieves the {@linkcode ProgramStateAccount}, creates it if it doesn't exist;
*/

View File

@ -1,7 +1,6 @@
import * as anchor from '@project-serum/anchor';
import * as spl from '@solana/spl-token';
import {
AccountInfo,
Commitment,
Keypair,
PublicKey,
@ -26,7 +25,8 @@ import { CrankAccount, CrankInitParams } from './crankAccount';
import { JobAccount, JobInitParams } from './jobAccount';
import { LeaseAccount } from './leaseAccount';
import { OracleAccount, OracleInitParams } from './oracleAccount';
import { PermissionAccount } from './permissionAccount';
import { PermissionAccount, PermissionSetParams } from './permissionAccount';
import { QueueDataBuffer } from './queueDataBuffer';
import { VrfAccount, VrfInitParams } from './vrfAccount';
/**
@ -35,13 +35,14 @@ import { VrfAccount, VrfInitParams } from './vrfAccount';
* A QueueAccount is responsible for allocating update requests to it's round robin queue of {@linkcode OracleAccount}'s.
*
* Data: {@linkcode types.OracleQueueAccountData}
* Buffer: Array<PublicKey>
*
* Buffer: {@linkcode QueueDataBuffer}
*/
export class QueueAccount extends Account<types.OracleQueueAccountData> {
static accountName = 'OracleQueueAccountData';
/** The public key of the queue's data buffer storing a list of oracle's that are actively heartbeating */
dataBuffer?: PublicKey;
/** The {@linkcode QueueDataBuffer} storing a list of oracle's that are actively heartbeating */
dataBuffer?: QueueDataBuffer;
/**
* Get the size of an {@linkcode QueueAccount} on-chain.
@ -78,30 +79,6 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
);
}
/**
* Invoke a callback each time a QueueAccount's oracle queue buffer has changed on-chain. The buffer stores a list of oracle's and their last heartbeat timestamp.
* @param callback - the callback invoked when the queues buffer changes
* @param commitment - optional, the desired transaction finality. defaults to 'confirmed'
* @returns the websocket subscription id
*/
onBufferChange(
callback: OnAccountChangeCallback<Array<PublicKey>>,
_dataBuffer?: PublicKey,
commitment: Commitment = 'confirmed'
): number {
const buffer = this.dataBuffer ?? _dataBuffer;
if (!buffer) {
throw new Error(
`No queue dataBuffer provided. Call queueAccount.loadData() or pass it to this function in order to watch the account for changes`
);
}
return this.program.connection.onAccountChange(
buffer,
accountInfo => callback(QueueAccount.decodeBuffer(accountInfo)),
commitment
);
}
/**
* Retrieve and decode the {@linkcode types.OracleQueueAccountData} stored in this account.
*/
@ -111,7 +88,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
this.publicKey
);
if (data === null) throw new errors.AccountNotFoundError(this.publicKey);
this.dataBuffer = data.dataBuffer;
this.dataBuffer = new QueueDataBuffer(this.program, data.dataBuffer);
return data;
}
@ -131,7 +108,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
params: QueueInitParams
): Promise<[TransactionObject, QueueAccount]> {
const queueKeypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(queueKeypair);
const dataBuffer = params.dataBufferKeypair ?? Keypair.generate();
program.verifyNewKeypair(dataBuffer);
const account = new QueueAccount(program, queueKeypair.publicKey);
const queueSize = params.queueSize ?? 500;
@ -224,12 +204,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
public async createOracleInstructions(
/** The publicKey of the account that will pay for the new accounts. Will also be used as the account authority if no other authority is provided. */
payer: PublicKey,
params: OracleInitParams & {
/** Whether to enable PERMIT_ORACLE_HEARTBEAT permissions. **Note:** Requires a provided queueAuthority keypair or payer to be the assigned queue authority. */
enable?: boolean;
/** Keypair used to enable heartbeat permissions if payer is not the queue authority. */
queueAuthority?: Keypair;
}
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[TransactionObject, OracleAccount]> {
const queue = await this.loadData();
@ -250,7 +225,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
const permissionSetTxn = permissionAccount.setInstruction(payer, {
permission: new PermitOracleHeartbeat(),
enable: true,
authority: params.queueAuthority,
queueAuthority: params.queueAuthority,
});
createPermissionTxnObject.combine(permissionSetTxn);
}
@ -265,12 +240,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
* Create a new {@linkcode OracleAccount} for the queue.
*/
public async createOracle(
params: OracleInitParams & {
/** Whether to enable PERMIT_ORACLE_HEARTBEAT permissions. **Note:** Requires a provided queueAuthority keypair or payer to be the assigned queue authority. */
enable?: boolean;
/** Keypair used to enable heartbeat permissions if payer is not the queue authority. */
queueAuthority?: Keypair;
}
params: OracleInitParams & Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[TransactionSignature, OracleAccount]> {
const signers: Keypair[] = [];
@ -293,88 +263,6 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
return [signature, oracleAccount];
}
/**
* Create a new {@linkcode AggregatorAccount} for the queue, along with its {@linkcode PermissionAccount} and {@linkcode LeaseAccount}.
*
* Optionally, specify a crankPubkey in order to push it onto an existing {@linkcode CrankAccount}.
*
* Optionally, enable the permissions by setting a queueAuthority keypair along with the enable boolean set to true.
*
* ```ts
* import {QueueAccount} from '@switchboard-xyz/solana.js';
* const queueAccount = new QueueAccount(program, queuePubkey);
* const [aggregatorInitSignatures, aggregatorAccount] =
await queueAccount.createFeed({
enable: true, // not needed if queue has unpermissionedFeedsEnabled
queueAuthority: queueAuthority, // not needed if queue has unpermissionedFeedsEnabled
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 60,
fundAmount: 2.5, // deposit 2.5 wSOL into the leaseAccount escrow
jobs: [
{ pubkey: jobAccount.publicKey },
{
weight: 2,
data: OracleJob.encodeDelimited(
OracleJob.fromObject({
tasks: [
{
valueTask: {
value: 1,
},
},
],
})
).finish(),
},
],
});
* ```
*/
public async createFeed(
params: Omit<
Omit<Omit<AggregatorInitParams, 'queueAccount'>, 'queueAuthority'>,
'authority'
> & {
authority?: Keypair;
crankPubkey?: PublicKey;
historyLimit?: number;
} & {
// lease params
fundAmount?: number;
funderAuthority?: Keypair;
funderTokenAccount?: PublicKey;
} & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
} & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
}
): Promise<[Array<TransactionSignature>, AggregatorAccount]> {
const signers: Keypair[] = [];
const queue = await this.loadData();
if (
params.queueAuthority &&
params.queueAuthority.publicKey.equals(queue.authority)
) {
signers.push(params.queueAuthority);
}
const [txns, aggregatorAccount] = await this.createFeedInstructions(
this.program.walletPubkey,
params
);
const signatures = await this.program.signAndSendAll(txns);
return [signatures, aggregatorAccount];
}
/**
* Create a new {@linkcode TransactionObject} containing the instructions and signers needed to create a new {@linkcode AggregatorAccount} for the queue along with its {@linkcode PermissionAccount} and {@linkcode LeaseAccount}.
*
@ -429,14 +317,10 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
fundAmount?: number;
funderAuthority?: Keypair;
funderTokenAccount?: PublicKey;
} & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
} & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
}
} & Partial<Omit<PermissionSetParams, 'permission'>> & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
}
): Promise<[TransactionObject[], AggregatorAccount]> {
const queue = await this.loadData();
@ -523,7 +407,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
const permissionSetTxn = permissionAccount.setInstruction(payer, {
permission: new PermitOracleQueueUsage(),
enable: true,
authority: params.queueAuthority,
queueAuthority: params.queueAuthority,
});
permissionInit.combine(permissionSetTxn);
}
@ -568,15 +452,82 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
return [packed, aggregatorAccount];
}
public async createCrank(
params: Omit<CrankInitParams, 'queueAccount'>
): Promise<[TransactionSignature, CrankAccount]> {
const [txn, crankAccount] = await this.createCrankInstructions(
/**
* Create a new {@linkcode AggregatorAccount} for the queue, along with its {@linkcode PermissionAccount} and {@linkcode LeaseAccount}.
*
* Optionally, specify a crankPubkey in order to push it onto an existing {@linkcode CrankAccount}.
*
* Optionally, enable the permissions by setting a queueAuthority keypair along with the enable boolean set to true.
*
* ```ts
* import {QueueAccount} from '@switchboard-xyz/solana.js';
* const queueAccount = new QueueAccount(program, queuePubkey);
* const [aggregatorInitSignatures, aggregatorAccount] =
await queueAccount.createFeed({
enable: true, // not needed if queue has unpermissionedFeedsEnabled
queueAuthority: queueAuthority, // not needed if queue has unpermissionedFeedsEnabled
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 60,
fundAmount: 2.5, // deposit 2.5 wSOL into the leaseAccount escrow
jobs: [
{ pubkey: jobAccount.publicKey },
{
weight: 2,
data: OracleJob.encodeDelimited(
OracleJob.fromObject({
tasks: [
{
valueTask: {
value: 1,
},
},
],
})
).finish(),
},
],
});
* ```
*/
public async createFeed(
params: Omit<
Omit<Omit<AggregatorInitParams, 'queueAccount'>, 'queueAuthority'>,
'authority'
> & {
authority?: Keypair;
crankPubkey?: PublicKey;
historyLimit?: number;
} & {
// lease params
fundAmount?: number;
funderAuthority?: Keypair;
funderTokenAccount?: PublicKey;
} & Partial<Omit<PermissionSetParams, 'permission'>> & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
}
): Promise<[Array<TransactionSignature>, AggregatorAccount]> {
const signers: Keypair[] = [];
const queue = await this.loadData();
if (
params.queueAuthority &&
params.queueAuthority.publicKey.equals(queue.authority)
) {
signers.push(params.queueAuthority);
}
const [txns, aggregatorAccount] = await this.createFeedInstructions(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(txn);
return [txnSignature, crankAccount];
const signatures = await this.program.signAndSendAll(txns);
return [signatures, aggregatorAccount];
}
public async createCrankInstructions(
@ -589,16 +540,76 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
});
}
public async createCrank(
params: Omit<CrankInitParams, 'queueAccount'>
): Promise<[TransactionSignature, CrankAccount]> {
const [txn, crankAccount] = await this.createCrankInstructions(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(txn);
return [txnSignature, crankAccount];
}
public async createVrfInstructions(
payer: PublicKey,
params: Omit<VrfInitParams, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[TransactionObject, VrfAccount]> {
const queue = await this.loadData();
const [vrfInit, vrfAccount] = await VrfAccount.createInstructions(
this.program,
payer,
{
vrfKeypair: params.vrfKeypair,
queueAccount: this,
callback: params.callback,
authority: params.authority,
}
);
// eslint-disable-next-line prefer-const
let [permissionInit, permissionAccount] =
PermissionAccount.createInstruction(this.program, payer, {
granter: this.publicKey,
grantee: vrfAccount.publicKey,
authority: queue.authority,
});
if (params.enable) {
if (params.queueAuthority || queue.authority.equals(payer)) {
const permissionSet = permissionAccount.setInstruction(payer, {
permission: new PermitOracleQueueUsage(),
enable: true,
queueAuthority: params.queueAuthority,
});
permissionInit = permissionInit.combine(permissionSet);
}
}
return [vrfInit.combine(permissionInit), vrfAccount];
}
public async createVrf(
params: Omit<VrfInitParams, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>>
): Promise<[TransactionSignature, VrfAccount]> {
const [txn, vrfAccount] = await this.createVrfInstructions(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(txn);
return [txnSignature, vrfAccount];
}
public async createBufferRelayerInstructions(
payer: PublicKey,
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
} & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>> & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
): Promise<[TransactionObject, BufferRelayerAccount]> {
const queue = await this.loadData();
@ -657,7 +668,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
const permissionSet = permissionAccount.setInstruction(payer, {
permission: new PermitOracleQueueUsage(),
enable: true,
authority: params.queueAuthority,
queueAuthority: params.queueAuthority,
});
permissionInit = permissionInit.combine(permissionSet);
}
@ -676,14 +687,11 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
}
public async createBufferRelayer(
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
} & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>> & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
): Promise<[TransactionSignature, BufferRelayerAccount]> {
const [txn, bufferRelayerAccount] =
await this.createBufferRelayerInstructions(
@ -694,111 +702,21 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
return [txnSignature, bufferRelayerAccount];
}
public async createVrfInstructions(
payer: PublicKey,
params: Omit<VrfInitParams, 'queueAccount'> & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
}
): Promise<[TransactionObject, VrfAccount]> {
const queue = await this.loadData();
const [vrfInit, vrfAccount] = await VrfAccount.createInstructions(
this.program,
payer,
{
vrfKeypair: params.vrfKeypair,
queueAccount: this,
callback: params.callback,
authority: params.authority,
}
);
// eslint-disable-next-line prefer-const
let [permissionInit, permissionAccount] =
PermissionAccount.createInstruction(this.program, payer, {
granter: this.publicKey,
grantee: vrfAccount.publicKey,
authority: queue.authority,
});
if (params.enable) {
if (params.queueAuthority || queue.authority.equals(payer)) {
const permissionSet = permissionAccount.setInstruction(payer, {
permission: new PermitOracleQueueUsage(),
enable: true,
authority: params.queueAuthority,
});
permissionInit = permissionInit.combine(permissionSet);
}
}
return [vrfInit.combine(permissionInit), vrfAccount];
}
public async createVrf(
params: Omit<VrfInitParams, 'queueAccount'> & {
// permission params
enable?: boolean;
queueAuthority?: Keypair;
}
): Promise<[TransactionSignature, VrfAccount]> {
const [txn, vrfAccount] = await this.createVrfInstructions(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(txn);
return [txnSignature, vrfAccount];
}
public static decodeBuffer(
bufferAccountInfo: AccountInfo<Buffer>
): Array<PublicKey> {
const buffer = bufferAccountInfo.data.slice(8) ?? Buffer.from('');
const oracles: PublicKey[] = [];
for (let i = 0; i < buffer.byteLength * 32; i += 32) {
if (buffer.byteLength - i < 32) {
break;
}
const pubkeyBuf = buffer.slice(i, i + 32);
const pubkey = new PublicKey(pubkeyBuf);
if (PublicKey.default.equals(pubkey)) {
break;
}
oracles.push(pubkey);
}
return oracles;
}
/** Load the list of oracles that are currently stored in the buffer */
public async loadOracles(
queue?: types.OracleQueueAccountData,
commitment: Commitment = 'confirmed'
): Promise<Array<PublicKey>> {
const dataBuffer =
this.dataBuffer ??
queue?.dataBuffer ??
(await this.loadData()).dataBuffer;
const accountInfo = await this.program.connection.getAccountInfo(
dataBuffer,
{ commitment }
);
if (!accountInfo || accountInfo.data === null) {
throw new errors.AccountNotFoundError(dataBuffer);
public async loadOracles(): Promise<Array<PublicKey>> {
let queue: QueueDataBuffer;
if (this.dataBuffer) {
queue = this.dataBuffer;
} else {
const queueData = await this.loadData();
queue = new QueueDataBuffer(this.program, queueData.dataBuffer);
}
return QueueAccount.decodeBuffer(accountInfo);
return queue.loadData();
}
/** Loads the oracle states for the oracles currently on the queue's dataBuffer */
public async loadOracleAccounts(
queue?: types.OracleQueueAccountData
): Promise<
public async loadOracleAccounts(): Promise<
Array<{
publicKey: PublicKey;
data: types.OracleAccountData;
@ -806,7 +724,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
> {
const coder = this.program.coder;
const oraclePubkeys = await this.loadOracles(queue);
const oraclePubkeys = await this.loadOracles();
const accountInfos = await anchor.utils.rpc.getMultipleAccounts(
this.program.connection,
oraclePubkeys
@ -846,7 +764,8 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
}>
> {
const queue = _queue ?? (await this.loadData());
const oracles = await this.loadOracleAccounts(queue);
const oracles = await this.loadOracleAccounts();
const timeout = queue.oracleTimeout;
// TODO: Use SolanaClock

View File

@ -0,0 +1,95 @@
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 list of oracles actively heartbeating on the queue
*
* Data: Array<{@linkcode PublicKey}>
*/
export class QueueDataBuffer extends Account<Array<PublicKey>> {
static accountName = 'QueueDataBuffer';
public size = 0;
/**
* Invoke a callback each time a QueueAccount's oracle queue buffer has changed on-chain. The buffer stores a list of oracle's and their last heartbeat timestamp.
* @param callback - the callback invoked when the queues buffer changes
* @param commitment - optional, the desired transaction finality. defaults to 'confirmed'
* @returns the websocket subscription id
*/
onChange(
callback: OnAccountChangeCallback<Array<PublicKey>>,
commitment: Commitment = 'confirmed'
): number {
if (this.publicKey.equals(PublicKey.default)) {
throw new Error(
`No queue 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(QueueDataBuffer.decode(accountInfo)),
commitment
);
}
/**
* Retrieve and decode the {@linkcode types.CrankAccountData} stored in this account.
*/
public async loadData(): Promise<Array<PublicKey>> {
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 = QueueDataBuffer.decode(accountInfo);
return data;
}
public static decode(
bufferAccountInfo: AccountInfo<Buffer>
): Array<PublicKey> {
const buffer = bufferAccountInfo.data.slice(8) ?? Buffer.from('');
const oracles: PublicKey[] = [];
for (let i = 0; i < buffer.byteLength * 32; i += 32) {
if (buffer.byteLength - i < 32) {
break;
}
const pubkeyBuf = buffer.slice(i, i + 32);
const pubkey = new PublicKey(pubkeyBuf);
if (PublicKey.default.equals(pubkey)) {
break;
}
oracles.push(pubkey);
}
return oracles;
}
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
): QueueDataBuffer {
if (crank.dataBuffer.equals(PublicKey.default)) {
throw new Error(`Failed to find crank data buffer`);
}
return new QueueDataBuffer(program, crank.dataBuffer);
}
}

View File

@ -2,6 +2,7 @@ import * as anchor from '@project-serum/anchor';
import * as spl from '@solana/spl-token';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
AccountInfo,
Commitment,
Keypair,
PublicKey,
@ -11,6 +12,7 @@ import {
TransactionInstruction,
TransactionSignature,
} from '@solana/web3.js';
import { promiseWithTimeout } from '@switchboard-xyz/common';
import * as errors from '../errors';
import * as types from '../generated';
import { SwitchboardProgram } from '../program';
@ -21,8 +23,10 @@ import { PermissionAccount } from './permissionAccount';
import { QueueAccount } from './queueAccount';
/**
* @class VrfAccount
* Account holding a Verifiable Random Function result with a callback instruction for consuming on-chain pseudo-randomness.
*
* Data: {@linkcode types.VrfAccountData}
* Result: [u8;32]
*/
export class VrfAccount extends Account<types.VrfAccountData> {
static accountName = 'VrfAccountData';
@ -69,6 +73,7 @@ export class VrfAccount extends Account<types.VrfAccountData> {
payer: PublicKey,
params: VrfInitParams
): Promise<[TransactionObject, VrfAccount]> {
program.verifyNewKeypair(params.vrfKeypair);
const vrfAccount = new VrfAccount(program, params.vrfKeypair.publicKey);
const size = program.account.vrfAccountData.size;
@ -353,6 +358,50 @@ export class VrfAccount extends Account<types.VrfAccountData> {
const txnSignature = await this.program.signAndSend(setCallbackTxn);
return txnSignature;
}
/** Await the next vrf round */
public async nextRound(
roundId: anchor.BN,
/** Number of milliseconds to await the next VRF round. */
timeout: number
): Promise<VrfResult> {
let ws: number | undefined = undefined;
return await promiseWithTimeout(
timeout,
new Promise((resolve: (result: VrfResult) => void) => {
ws = this.program.connection.onAccountChange(
this.publicKey,
(accountInfo: AccountInfo<Buffer>) => {
const vrf = types.VrfAccountData.decode(accountInfo.data);
if (!vrf.counter.eq(roundId)) {
return;
}
if (
vrf.status.kind === 'StatusCallbackSuccess' ||
vrf.status.kind === 'StatusVerifyFailure'
) {
resolve({
success: vrf.status.kind === 'StatusCallbackSuccess',
result: new Uint8Array(vrf.currentRound.result),
status: vrf.status,
});
}
}
);
})
).finally(async () => {
if (ws) {
await this.program.connection.removeAccountChangeListener(ws);
}
ws = undefined;
});
}
}
export interface VrfResult {
success: boolean;
result: Uint8Array;
status: types.VrfStatusKind;
}
/**

View File

@ -12,6 +12,12 @@ export class SwitchboardProgramReadOnlyError extends Error {
Object.setPrototypeOf(this, SwitchboardProgramReadOnlyError.prototype);
}
}
export class ExistingKeypair extends Error {
constructor() {
super('Provided keypair corresponds to an existing account.');
Object.setPrototypeOf(this, ExistingKeypair.prototype);
}
}
export class AccountNotFoundError extends Error {
constructor(publicKey: anchor.web3.PublicKey) {
super(`No account was found at the address: ${publicKey.toBase58()}`);

View File

@ -183,6 +183,13 @@ export class SwitchboardProgram {
}
}
public async verifyNewKeypair(keypair: Keypair): Promise<void> {
const accountInfo = await this.connection.getAccountInfo(keypair.publicKey);
if (accountInfo) {
throw new errors.ExistingKeypair();
}
}
public get account(): anchor.AccountNamespace {
return this._program.account;
}