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

789 lines
22 KiB
TypeScript

import * as anchor from '@project-serum/anchor';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
AccountMeta,
Commitment,
Keypair,
PublicKey,
SystemProgram,
TransactionSignature,
} from '@solana/web3.js';
import * as errors from '../errors';
import * as types from '../generated';
import { SwitchboardProgram } from '../SwitchboardProgram';
import {
TransactionObject,
TransactionObjectOptions,
} from '../TransactionObject';
import { Account, OnAccountChangeCallback } from './account';
import { AggregatorAccount, AggregatorPdaAccounts } from './aggregatorAccount';
import { CrankDataBuffer } from './crankDataBuffer';
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: {@linkcode CrankDataBuffer}
*/
export class CrankAccount extends Account<types.CrankAccountData> {
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?: CrankDataBuffer;
/**
* Get the size of an {@linkcode CrankAccount} on-chain.
*/
public size = this.program.account.crankAccountData.size;
/**
* Return a crank account initialized to the default values.
*/
public static default(): types.CrankAccountData {
const buffer = Buffer.alloc(432, 0);
types.CrankAccountData.discriminator.copy(buffer, 0);
return types.CrankAccountData.decode(buffer);
}
/**
* Invoke a callback each time a CrankAccount's data has changed on-chain.
* @param callback - the callback invoked when the cranks state changes
* @param commitment - optional, the desired transaction finality. defaults to 'confirmed'
* @returns the websocket subscription id
*/
onChange(
callback: OnAccountChangeCallback<types.CrankAccountData>,
commitment: Commitment = 'confirmed'
): number {
return this.program.connection.onAccountChange(
this.publicKey,
accountInfo => callback(types.CrankAccountData.decode(accountInfo.data)),
commitment
);
}
/** Load an existing CrankAccount with its current on-chain state */
public static async load(
program: SwitchboardProgram,
publicKey: PublicKey | string
): Promise<[CrankAccount, types.CrankAccountData]> {
const account = new CrankAccount(
program,
typeof publicKey === 'string' ? new PublicKey(publicKey) : publicKey
);
const state = await account.loadData();
return [account, state];
}
/**
* Retrieve and decode the {@linkcode types.CrankAccountData} stored in this account.
*/
public async loadData(): Promise<types.CrankAccountData> {
const data = await types.CrankAccountData.fetch(
this.program,
this.publicKey
);
if (data === null)
throw new errors.AccountNotFoundError('Crank', this.publicKey);
if (!this.dataBuffer) {
this.dataBuffer = CrankDataBuffer.fromCrank(this.program, data);
}
return data;
}
public static async createInstructions(
program: SwitchboardProgram,
payer: PublicKey,
params: CrankInitParams
): Promise<[CrankAccount, TransactionObject]> {
const keypair = params.keypair ?? Keypair.generate();
program.verifyNewKeypair(keypair);
const buffer = params.dataBufferKeypair ?? anchor.web3.Keypair.generate();
program.verifyNewKeypair(buffer);
const maxRows = params.maxRows ?? 500;
const crankSize = CrankDataBuffer.getAccountSize(maxRows);
const crankInit = new TransactionObject(
payer,
[
SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: buffer.publicKey,
space: crankSize,
lamports: await program.connection.getMinimumBalanceForRentExemption(
crankSize
),
programId: program.programId,
}),
types.crankInit(
program,
{
params: {
name: Buffer.from(params.name ?? '').slice(0, 32),
metadata: Buffer.from(params.metadata ?? '').slice(0, 64),
crankSize: maxRows,
},
},
{
crank: keypair.publicKey,
queue: params.queueAccount.publicKey,
buffer: buffer.publicKey,
systemProgram: SystemProgram.programId,
payer: payer,
}
),
],
[keypair, buffer]
);
const crankAccount = new CrankAccount(program, keypair.publicKey);
crankAccount.dataBuffer = new CrankDataBuffer(program, buffer.publicKey);
return [crankAccount, crankInit];
}
public static async create(
program: SwitchboardProgram,
params: CrankInitParams
): Promise<[CrankAccount, TransactionSignature]> {
const [crankAccount, crankInit] = await CrankAccount.createInstructions(
program,
program.walletPubkey,
params
);
const txnSignature = await program.signAndSend(crankInit);
return [crankAccount, txnSignature];
}
/**
* Pushes a new aggregator onto the crank.
* @param params The crank push parameters.
* @return TransactionSignature
*/
async pushInstruction(
payer: PublicKey,
params: CrankPushParams
): Promise<TransactionObject> {
const crankData = await this.loadData();
const queueAccount = new QueueAccount(this.program, crankData.queuePubkey);
const queue = await queueAccount.loadData();
const { permissionAccount, permissionBump, leaseAccount, leaseEscrow } =
params.aggregatorAccount.getAccounts(queueAccount, queue.authority);
return new TransactionObject(
payer,
[
types.crankPush(
this.program,
{
params: {
stateBump: this.program.programState.bump,
permissionBump: permissionBump,
notifiRef: null,
},
},
{
crank: this.publicKey,
aggregator: params.aggregatorAccount.publicKey,
oracleQueue: queueAccount.publicKey,
queueAuthority: queue.authority,
permission: permissionAccount.publicKey,
lease: leaseAccount.publicKey,
escrow: leaseEscrow,
programState: this.program.programState.publicKey,
dataBuffer:
this.dataBuffer?.publicKey ?? (await this.loadData()).dataBuffer,
}
),
],
[]
);
}
/**
* Pushes a new aggregator onto the crank.
* @param params The crank push parameters.
* @return TransactionSignature
*/
async push(params: CrankPushParams): Promise<TransactionSignature> {
const pushTxn = await this.pushInstruction(
this.program.walletPubkey,
params
);
const txnSignature = await this.program.signAndSend(pushTxn);
return txnSignature;
}
public async popInstruction(
payer: PublicKey,
params: CrankPopParams
): Promise<TransactionObject> {
const next =
params.readyPubkeys ??
(await this.peakNextReady(5, params.unixTimestamp));
if (next.length === 0) throw new Error('Crank is not ready to be turned.');
const remainingAccounts: PublicKey[] = [];
const leaseBumpsMap: Map<string, number> = new Map();
const permissionBumpsMap: Map<string, number> = new Map();
const crankData = await this.loadData();
const queueAccount = new QueueAccount(this.program, crankData.queuePubkey);
const queueData = await queueAccount.loadData();
const toBumps = (bumpMap: Map<string, number>) =>
new Uint8Array(Array.from(bumpMap.values()));
for (const row of next) {
const aggregatorAccount = new AggregatorAccount(this.program, row);
const {
leaseAccount,
leaseBump,
permissionAccount,
permissionBump,
leaseEscrow,
} = aggregatorAccount.getAccounts(queueAccount, queueData.authority);
remainingAccounts.push(aggregatorAccount.publicKey);
remainingAccounts.push(leaseAccount.publicKey);
remainingAccounts.push(leaseEscrow);
remainingAccounts.push(permissionAccount.publicKey);
leaseBumpsMap.set(row.toBase58(), leaseBump);
permissionBumpsMap.set(row.toBase58(), permissionBump);
}
remainingAccounts.sort((a: PublicKey, b: PublicKey) =>
a.toBuffer().compare(b.toBuffer())
);
const payoutWallet =
params?.payoutWallet ?? this.program.mint.getAssociatedAddress(payer);
const crankPopIxn = types.crankPop(
this.program,
{
params: {
stateBump: this.program.programState.bump,
leaseBumps: toBumps(leaseBumpsMap),
permissionBumps: toBumps(permissionBumpsMap),
nonce: params.nonce ?? null,
failOpenOnAccountMismatch: false,
},
},
{
crank: this.publicKey,
oracleQueue: queueAccount.publicKey,
queueAuthority: queueData.authority,
programState: this.program.programState.publicKey,
payoutWallet: payoutWallet,
tokenProgram: TOKEN_PROGRAM_ID,
crankDataBuffer: crankData.dataBuffer,
queueDataBuffer: queueData.dataBuffer,
mint: this.program.mint.address,
}
);
crankPopIxn.keys.push(
...remainingAccounts.map((pubkey): AccountMeta => {
return { isSigner: false, isWritable: true, pubkey };
})
);
const txnObject: TransactionObject = new TransactionObject(
payer,
[crankPopIxn],
[]
);
return txnObject;
}
public async pop(params: CrankPopParams): Promise<TransactionSignature> {
const popTxn = await this.popInstruction(this.program.walletPubkey, params);
const txnSignature = await this.program.signAndSend(popTxn);
return txnSignature;
}
private getPopTxn(
payer: PublicKey,
params: {
payoutTokenWallet: PublicKey;
queuePubkey: PublicKey;
queueAuthority: PublicKey;
queueDataBuffer: PublicKey;
crankDataBuffer: PublicKey;
remainingAccounts: Array<PublicKey>;
leaseBumps: Map<string, number>;
permissionBumps: Map<string, number>;
nonce?: number;
failOpenOnMismatch?: boolean;
},
options?: TransactionObjectOptions
) {
const remainingAccounts = params.remainingAccounts.sort(
(a: PublicKey, b: PublicKey) => a.toBuffer().compare(b.toBuffer())
);
const leaseBumps: Array<number> = [];
const permissionBumps: Array<number> = [];
for (const remainingAccount of remainingAccounts) {
leaseBumps.push(params.leaseBumps.get(remainingAccount.toBase58()) ?? 0);
permissionBumps.push(
params.permissionBumps.get(remainingAccount.toBase58()) ?? 0
);
}
const crankPopIxn = types.crankPop(
this.program,
{
params: {
stateBump: this.program.programState.bump,
leaseBumps: new Uint8Array(leaseBumps),
permissionBumps: new Uint8Array(permissionBumps),
nonce: params.nonce ?? null,
failOpenOnAccountMismatch: params.failOpenOnMismatch ?? false,
},
},
{
crank: this.publicKey,
oracleQueue: params.queuePubkey,
queueAuthority: params.queueAuthority,
programState: this.program.programState.publicKey,
payoutWallet: params.payoutTokenWallet,
tokenProgram: TOKEN_PROGRAM_ID,
crankDataBuffer: params.crankDataBuffer,
queueDataBuffer: params.queueDataBuffer,
mint: this.program.mint.address,
}
);
crankPopIxn.keys.push(
...remainingAccounts.map((pubkey): AccountMeta => {
return { isSigner: false, isWritable: true, pubkey };
})
);
return new TransactionObject(payer, [crankPopIxn], [], options);
}
public popSync(
payer: PublicKey,
params: {
payoutTokenWallet: PublicKey;
queuePubkey: PublicKey;
queueAuthority: PublicKey;
queueDataBuffer: PublicKey;
crankDataBuffer: PublicKey;
readyAggregators: Array<[AggregatorAccount, AggregatorPdaAccounts]>;
nonce?: number;
failOpenOnMismatch?: boolean;
popIdx?: number;
},
options?: TransactionObjectOptions
): TransactionObject {
if (params.readyAggregators.length < 1) {
throw new Error(`No aggregators ready`);
}
let remainingAccounts: PublicKey[] = [];
let txn: TransactionObject | undefined = undefined;
const allLeaseBumps = params.readyAggregators.reduce(
(map, [aggregatorAccount, pdaAccounts]) => {
map.set(aggregatorAccount.publicKey.toBase58(), pdaAccounts.leaseBump);
return map;
},
new Map<string, number>()
);
const allPermissionBumps = params.readyAggregators.reduce(
(map, [aggregatorAccount, pdaAccounts]) => {
map.set(
aggregatorAccount.publicKey.toBase58(),
pdaAccounts.permissionBump
);
return map;
},
new Map<string, number>()
);
// add as many readyAggregators until the txn overflows
for (const [
readyAggregator,
aggregatorPdaAccounts,
] of params.readyAggregators) {
const { leaseAccount, leaseEscrow, permissionAccount } =
aggregatorPdaAccounts;
const newRemainingAccounts = [
...remainingAccounts,
readyAggregator.publicKey,
leaseAccount.publicKey,
leaseEscrow,
permissionAccount.publicKey,
];
try {
const newTxn = this.getPopTxn(
payer,
{
...params,
remainingAccounts: newRemainingAccounts,
leaseBumps: allLeaseBumps,
permissionBumps: allPermissionBumps,
},
options
);
// succeeded, so set running txn and remaining accounts and try again
txn = newTxn;
remainingAccounts = newRemainingAccounts;
} catch (error) {
if (error instanceof errors.TransactionOverflowError) {
if (txn === undefined) {
throw new Error(`Failed to create crank pop transaction, ${error}`);
}
return txn;
}
throw error;
}
}
if (txn === undefined) {
throw new Error(`Failed to create crank pop transaction`);
}
return txn;
}
public packAndPopInstructions(
payer: PublicKey,
params: {
payoutTokenWallet: PublicKey;
queuePubkey: PublicKey;
queueAuthority: PublicKey;
queueDataBuffer: PublicKey;
crankDataBuffer: PublicKey;
readyAggregators: Array<[AggregatorAccount, AggregatorPdaAccounts]>;
nonce?: number;
failOpenOnMismatch?: boolean;
},
options?: TransactionObjectOptions
): Array<TransactionObject> {
const numReady = params.readyAggregators.length;
if (numReady < 6) {
// send as-is
return Array.from(Array(numReady).keys()).map(() => {
return this.popSync(
payer,
{
...params,
readyAggregators: params.readyAggregators,
nonce: Math.random(),
},
options
);
});
} else {
// stagger the ready accounts
return Array.from(Array(numReady).keys()).map(n => {
return this.popSync(
payer,
{
...params,
readyAggregators: params.readyAggregators.slice(Math.max(0, n - 4)),
nonce: Math.random(),
},
options
);
});
}
}
public async packAndPop(
params: {
payoutTokenWallet: PublicKey;
queuePubkey: PublicKey;
queueAuthority: PublicKey;
queueDataBuffer: PublicKey;
crankDataBuffer: PublicKey;
readyAggregators: Array<[AggregatorAccount, AggregatorPdaAccounts]>;
nonce?: number;
failOpenOnMismatch?: boolean;
},
options?: TransactionObjectOptions
): Promise<Array<TransactionSignature>> {
const popTxns = this.packAndPopInstructions(
this.program.walletPubkey,
params,
options
);
const txnSignatures = await this.program.signAndSendAll(
popTxns,
{
skipPreflight: true,
skipConfrimation: true,
},
undefined,
10 // 10ms delay between txns to help ordering
);
return txnSignatures;
}
/**
* Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
* @param num The limit of pubkeys to return.
* @return List of {@linkcode types.CrankRow}, ordered by timestamp.
*/
async peakNextWithTime(num?: number): Promise<types.CrankRow[]> {
const crankRows = await this.loadCrank();
return crankRows.slice(0, num ?? crankRows.length);
}
/**
* Get an array of the next readily updateable aggregator pubkeys to be popped
* from the crank, limited by n
* @param num The limit of pubkeys to return.
* @return Pubkey list of Aggregator pubkeys.
*/
async peakNextReady(
num?: number,
unixTimestamp?: number
): Promise<PublicKey[]> {
const now = unixTimestamp ?? Math.floor(Date.now() / 1000);
const crankRows = await this.peakNextWithTime(num);
return crankRows
.filter(row => now >= row.nextTimestamp.toNumber())
.map(row => row.pubkey);
}
/**
* Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
* @param num The limit of pubkeys to return.
* @return Pubkey list of Aggregators next up to be popped.
*/
async peakNext(num?: number): Promise<PublicKey[]> {
const crankRows = await this.peakNextWithTime(num);
return crankRows.map(row => row.pubkey);
}
/**
* Load a cranks {@linkcode CrankDataBuffer}.
* @return the list of aggregtors and their next available update time.
*/
async loadCrank(sorted = true): Promise<Array<types.CrankRow>> {
if (!this.dataBuffer) {
this.dataBuffer = new CrankDataBuffer(
this.program,
(await this.loadData()).dataBuffer
);
}
const crankRows = await this.dataBuffer.loadData();
if (sorted) {
return CrankDataBuffer.sort(crankRows);
}
return crankRows;
}
getCrankAccounts(
crankRows: Array<types.CrankRow>,
queueAccount: QueueAccount,
queueAuthority: PublicKey
): Map<string, AggregatorPdaAccounts> {
const crankAccounts: Map<string, AggregatorPdaAccounts> = new Map();
for (const row of crankRows) {
const aggregatorAccount = new AggregatorAccount(this.program, row.pubkey);
const accounts = aggregatorAccount.getAccounts(
queueAccount,
queueAuthority
);
crankAccounts.set(aggregatorAccount.publicKey.toBase58(), accounts);
}
return crankAccounts;
}
/** Whether an aggregator pubkey is active on a Crank */
async isOnCrank(
pubkey: PublicKey,
crankRows?: Array<types.CrankRow>
): Promise<boolean> {
const rows = crankRows ?? (await this.loadCrank());
const idx = rows.findIndex(r => r.pubkey.equals(pubkey));
if (idx === -1) {
return false;
}
return true;
}
public async fetchAccounts(
_crank?: types.CrankAccountData,
_queueAccount?: QueueAccount,
_queue?: types.OracleQueueAccountData
): Promise<CrankAccounts> {
const crank = _crank ?? (await this.loadData());
const queueAccount =
_queueAccount ?? new QueueAccount(this.program, crank.queuePubkey);
const queue = _queue ?? (await queueAccount.loadData());
const crankRows = await this.loadCrank();
const aggregatorPubkeys = crankRows.map(r => r.pubkey);
const aggregators = await AggregatorAccount.fetchMultiple(
this.program,
aggregatorPubkeys
);
return {
crank: {
publicKey: this.publicKey,
data: crank,
},
queue: {
publicKey: queueAccount.publicKey,
data: queue,
},
dataBuffer: {
publicKey: crank.dataBuffer,
data: crankRows,
},
aggregators: aggregators.map(a => {
return {
publicKey: a.account.publicKey,
data: a.data,
};
}),
};
}
public async toAccountsJSON(
_crank?: types.CrankAccountData,
_crankRows?: Array<types.CrankRow>
): Promise<CrankAccountsJSON> {
const crank = _crank ?? (await this.loadData());
const crankRows = _crankRows ?? (await this.loadCrank());
return {
publicKey: this.publicKey,
...crank.toJSON(),
dataBuffer: {
publicKey: crank.dataBuffer,
data: crankRows,
},
};
}
}
/**
* Parameters for initializing a CrankAccount
*/
export interface CrankInitParams {
/**
* OracleQueueAccount for which this crank is associated
*/
queueAccount: QueueAccount;
/**
* String specifying crank name
*/
name?: string;
/**
* String specifying crank metadata
*/
metadata?: String;
/**
* Optional max number of rows
*/
maxRows?: number;
/**
* Optional
*/
keypair?: Keypair;
/**
* Optional
*/
dataBufferKeypair?: Keypair;
}
/**
* Parameters for pushing an element into a CrankAccount.
*/
export interface CrankPushParams {
/**
* Specifies the aggregator to push onto the crank.
*/
aggregatorAccount: AggregatorAccount;
}
/**
* Parameters for popping an element from a CrankAccount.
*/
export interface CrankPopParams {
/**
* Specifies the wallet to reward for turning the crank.
*
* Defaults to the payer.
*/
payoutWallet?: PublicKey;
/**
* Array of pubkeys to attempt to pop. If discluded, this will be loaded
* from the crank upon calling.
*/
readyPubkeys?: PublicKey[];
/**
* Nonce to allow consecutive crank pops with the same blockhash.
*/
nonce?: number;
failOpenOnMismatch?: boolean;
popIdx?: number;
/**
* Unix timestamp to use to determine readyPubkeys (if not provided)
*/
unixTimestamp?: number;
}
export type CrankAccountsJSON = Omit<
types.CrankAccountDataJSON,
'dataBuffer'
> & {
publicKey: PublicKey;
dataBuffer: { publicKey: PublicKey; data: Array<types.CrankRow> };
};
export type CrankAccounts = {
crank: {
publicKey: PublicKey;
data: types.CrankAccountData;
};
queue: {
publicKey: PublicKey;
data: types.OracleQueueAccountData;
};
dataBuffer: {
publicKey: PublicKey;
data: Array<types.CrankRow>;
};
aggregators: Array<{
publicKey: PublicKey;
data: types.AggregatorAccountData;
}>;
};