solana.js: added aggregator queue transfer method

This commit is contained in:
Conner Gallagher 2022-12-05 14:36:51 -07:00
parent 2c8e676c9b
commit 9620ce3bc9
6 changed files with 445 additions and 82 deletions

View File

@ -19,11 +19,13 @@ import crypto from 'crypto';
import { JobAccount } from './jobAccount';
import { QueueAccount } from './queueAccount';
import { LeaseAccount } from './leaseAccount';
import { PermissionAccount } from './permissionAccount';
import { PermissionAccount, PermissionSetParams } 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';
import { PermitOracleQueueUsage } from '../generated/types/SwitchboardPermission';
import { CrankAccount } from './crankAccount';
/**
* Account type holding a data feed's update configuration, job accounts, and its current result.
@ -233,6 +235,172 @@ export class AggregatorAccount extends Account<types.AggregatorAccountData> {
return [account, txnSignature];
}
async transferInstruction(
payer: PublicKey,
params: {
newQueue: QueueAccount;
authority?: Keypair;
crankPubkey?: PublicKey;
} & Partial<PermissionSetParams>
): Promise<Array<TransactionObject>> {
const txns: Array<TransactionObject> = [];
const aggregator = await this.loadData();
const newQueue = await params.newQueue.loadData();
const authorityPubkey = params.authority
? params.authority.publicKey
: payer;
if (!aggregator.authority.equals(authorityPubkey)) {
throw new errors.IncorrectAuthority(
aggregator.authority,
authorityPubkey
);
}
const jobs = await this.loadJobs(aggregator);
const jobAuthorities = jobs.map(j => j.state.authority);
// const jobAuthorities = Array.from(
// jobs.reduce((set, job) => {
// set.add(job.state.authority);
// return set;
// }, new Set<PublicKey>())
// );
const [oldLeaseAccount] = LeaseAccount.fromSeed(
this.program,
aggregator.queuePubkey,
this.publicKey
);
const setQueueTxn = new TransactionObject(
payer,
[
types.aggregatorSetQueue(
this.program,
{ params: {} },
{
aggregator: this.publicKey,
authority: authorityPubkey,
queue: params.newQueue.publicKey,
}
),
],
params.authority ? [params.authority] : []
);
txns.push(setQueueTxn);
// create and set permissions
const [newPermissionAccount, permissionInitTxn] =
PermissionAccount.createInstruction(this.program, payer, {
authority: newQueue.authority,
granter: params.newQueue.publicKey,
grantee: this.publicKey,
});
if (params.enable) {
if (
params.queueAuthority &&
!params.queueAuthority.publicKey.equals(newQueue.authority)
) {
throw new errors.IncorrectAuthority(
newQueue.authority,
params.queueAuthority.publicKey
);
}
const permissionSetTxn = newPermissionAccount.setInstruction(payer, {
enable: true,
queueAuthority: params.queueAuthority ?? undefined,
permission: new PermitOracleQueueUsage(),
});
permissionInitTxn.combine(permissionSetTxn);
}
txns.push(permissionInitTxn);
// create payer token account if we need to
const payerTokenAccount = this.program.mint.getAssociatedAddress(payer);
const payerTokenAccountInfo = await this.program.connection.getAccountInfo(
payerTokenAccount
);
if (payerTokenAccountInfo === null) {
const [payerTokenAddress, payerTokenInitTxn] =
await this.program.mint.getOrCreateWrappedUserInstructions(payer, {
amount: 0,
});
txns.unshift(payerTokenInitTxn);
}
// withdraw from lease
let oldLeaseWithdrawAuthority = payer;
let oldLeaseBalance = 0;
try {
const oldLease = await oldLeaseAccount.loadData();
oldLeaseBalance = await oldLeaseAccount.getBalance(oldLease.escrow);
oldLeaseWithdrawAuthority = oldLease.withdrawAuthority;
const withdrawTxn = await oldLeaseAccount.withdrawInstruction(payer, {
amount: oldLeaseBalance,
withdrawWallet: payerTokenAccount,
unwrap: false,
});
txns.push(withdrawTxn);
} catch {
// failed to get old lease balance, skipping
}
// create lease for the new queue and transfer existing balance
const [leaseAccount, leaseInitTxn] = await LeaseAccount.createInstructions(
this.program,
payer,
{
aggregatorAccount: this,
queueAccount: params.newQueue,
jobAuthorities,
loadAmount: oldLeaseBalance ?? 0,
withdrawAuthority: oldLeaseWithdrawAuthority,
jobPubkeys: aggregator.jobPubkeysData.slice(
0,
aggregator.jobPubkeysSize
),
}
);
txns.push(leaseInitTxn);
// push onto crank
if (params.crankPubkey) {
const crankAccount = new CrankAccount(this.program, params.crankPubkey);
const crank = await crankAccount.loadData();
if (!params.newQueue.publicKey.equals(crank.queuePubkey)) {
throw new Error(
`Desired crank does not belong to new queue, expected ${params.newQueue.publicKey}, received ${crank.queuePubkey}`
);
}
const crankPush = await crankAccount.pushInstruction(payer, {
aggregatorAccount: this,
});
txns.push(crankPush);
}
return TransactionObject.pack(txns);
}
async transfer(
params: {
newQueue: QueueAccount;
authority?: Keypair;
crankPubkey?: PublicKey;
} & Partial<PermissionSetParams>
): Promise<Array<TransactionSignature>> {
const transactions = await this.transferInstruction(
this.program.walletPubkey,
params
);
const txnSignatures = await this.program.signAndSendAll(transactions, {
skipPreflight: true,
});
return txnSignatures;
}
public getAccounts(params: {
queueAccount: QueueAccount;
queueAuthority: PublicKey;
@ -439,7 +607,15 @@ export class AggregatorAccount extends Account<types.AggregatorAccountData> {
* @throws {AggregatorConfigError} if minUpdateDelaySeconds < 5, if batchSize > queueSize, if minOracleResults > batchSize, if minJobResults > aggregator.jobPubkeysSize
*/
public verifyConfig(
aggregator: types.AggregatorAccountData,
aggregator:
| types.AggregatorAccountData
| {
oracleRequestBatchSize: number;
minOracleResults: number;
minJobResults: number;
minUpdateDelaySeconds: number;
jobPubkeysSize: number;
},
queue: types.OracleQueueAccountData,
target: {
batchSize?: number;

View File

@ -5,6 +5,7 @@ import { SwitchboardProgram } from '../program';
import { Account } from './account';
import * as spl from '@solana/spl-token';
import {
AccountMeta,
Keypair,
PublicKey,
SystemProgram,
@ -78,31 +79,25 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
static getWallets(
jobAuthorities: Array<PublicKey>,
mint: PublicKey
): {
wallets: Array<{ publicKey: PublicKey; bump: number }>;
walletBumps: Uint8Array;
} {
): Array<{ publicKey: PublicKey; bump: number }> {
const wallets: Array<{ publicKey: PublicKey; bump: number }> = [];
const walletBumps: Array<number> = [];
for (const jobAuthority in jobAuthorities) {
const authority = new PublicKey(jobAuthority);
if (!jobAuthority || PublicKey.default.equals(authority)) {
for (const jobAuthority of jobAuthorities) {
if (!jobAuthority || PublicKey.default.equals(jobAuthority)) {
continue;
}
const [jobWallet, bump] = anchor.utils.publicKey.findProgramAddressSync(
[
authority.toBuffer(),
jobAuthority.toBuffer(),
spl.TOKEN_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
wallets.push({ publicKey: jobWallet, bump });
walletBumps.push(bump);
}
return { wallets, walletBumps: new Uint8Array(walletBumps) };
return wallets;
}
static async createInstructions(
@ -111,11 +106,12 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
params: {
aggregatorAccount: AggregatorAccount;
queueAccount: QueueAccount;
jobAuthorities?: Array<PublicKey>;
jobPubkeys?: Array<PublicKey>;
loadAmount?: number;
funderTokenAccount?: PublicKey;
funderAuthority?: Keypair;
withdrawAuthority?: PublicKey;
jobAuthorities: Array<PublicKey>;
}
): Promise<[LeaseAccount, TransactionObject]> {
const loadAmount = params.loadAmount ?? 0;
@ -152,46 +148,68 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
spl.ASSOCIATED_TOKEN_PROGRAM_ID
);
const { walletBumps } = LeaseAccount.getWallets(
params.jobAuthorities,
// load jobPubkeys and authorities ONLY if undefined
// we need to allow empty arrays for initial job creation or else loading aggregator will fail
let jobPubkeys = params.jobPubkeys;
let jobAuthorities = params.jobAuthorities;
if (jobPubkeys === undefined || jobAuthorities === undefined) {
const aggregator = await params.aggregatorAccount.loadData();
jobPubkeys = aggregator.jobPubkeysData.slice(
0,
aggregator.jobPubkeysSize
);
const jobs = await params.aggregatorAccount.loadJobs(aggregator);
jobAuthorities = jobs.map(j => j.state.authority);
}
const wallets = LeaseAccount.getWallets(
jobAuthorities ?? [],
program.mint.address
);
const walletBumps = new Uint8Array(wallets.map(w => w.bump));
const remainingAccounts: Array<AccountMeta> = (jobPubkeys ?? [])
.concat(wallets.map(w => w.publicKey))
.map(pubkey => {
return { isSigner: false, isWritable: true, pubkey };
});
const createTokenAccountIxn = spl.createAssociatedTokenAccountInstruction(
payer,
escrow,
leaseAccount.publicKey,
program.mint.address
);
const leaseInitIxn = types.leaseInit(
program,
{
params: {
loadAmount: loadTokenAmountBN,
withdrawAuthority: params.withdrawAuthority ?? payer,
leaseBump: leaseBump,
stateBump: program.programState.bump,
walletBumps: walletBumps,
},
},
{
lease: leaseAccount.publicKey,
queue: params.queueAccount.publicKey,
aggregator: params.aggregatorAccount.publicKey,
payer: payer,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
funder: funderTokenAccount,
owner: funderAuthority,
escrow: escrow,
programState: program.programState.publicKey,
mint: program.mint.address,
}
);
leaseInitIxn.keys.push(...remainingAccounts);
const leaseInitTxn = new TransactionObject(
payer,
[
spl.createAssociatedTokenAccountInstruction(
payer,
escrow,
leaseAccount.publicKey,
program.mint.address
),
types.leaseInit(
program,
{
params: {
loadAmount: loadTokenAmountBN,
withdrawAuthority: params.withdrawAuthority ?? payer,
leaseBump: leaseBump,
stateBump: program.programState.bump,
walletBumps: walletBumps,
},
},
{
lease: leaseAccount.publicKey,
queue: params.queueAccount.publicKey,
aggregator: params.aggregatorAccount.publicKey,
payer: payer,
systemProgram: SystemProgram.programId,
tokenProgram: spl.TOKEN_PROGRAM_ID,
funder: funderTokenAccount,
owner: funderAuthority,
escrow: escrow,
programState: program.programState.publicKey,
mint: program.mint.address,
}
),
],
[createTokenAccountIxn, leaseInitIxn],
params.funderAuthority ? [params.funderAuthority] : []
);
@ -208,11 +226,12 @@ export class LeaseAccount extends Account<types.LeaseAccountData> {
params: {
aggregatorAccount: AggregatorAccount;
queueAccount: QueueAccount;
jobAuthorities?: Array<PublicKey>;
jobPubkeys?: Array<PublicKey>;
loadAmount?: number;
funderTokenAccount?: PublicKey;
funderAuthority?: Keypair;
withdrawAuthority?: PublicKey;
jobAuthorities: Array<PublicKey>;
}
): Promise<[LeaseAccount, TransactionSignature]> {
const [leaseAccount, transaction] = await LeaseAccount.createInstructions(

View File

@ -1,5 +1,7 @@
import * as anchor from '@project-serum/anchor';
import { ACCOUNT_DISCRIMINATOR_SIZE } from '@project-serum/anchor';
import {
AccountInfo,
Keypair,
PublicKey,
SystemProgram,
@ -49,8 +51,6 @@ export class PermitNone {
}
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. */
@ -196,7 +196,12 @@ export class PermissionAccount extends Account<types.PermissionAccountData> {
/**
* Sets the permission in the PermissionAccount
*/
public async set(params: PermissionSetParams): Promise<string> {
public async set(
params: PermissionSetParams & {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: types.SwitchboardPermissionKind;
}
): Promise<string> {
const setTxn = this.setInstruction(this.program.walletPubkey, params);
const txnSignature = await this.program.signAndSend(setTxn);
return txnSignature;
@ -207,7 +212,10 @@ export class PermissionAccount extends Account<types.PermissionAccountData> {
*/
public setInstruction(
payer: PublicKey,
params: PermissionSetParams
params: PermissionSetParams & {
/** The {@linkcode types.SwitchboardPermission} to set for the grantee. */
permission: types.SwitchboardPermissionKind;
}
): TransactionObject {
return new TransactionObject(
payer,
@ -231,4 +239,37 @@ export class PermissionAccount extends Account<types.PermissionAccountData> {
params.queueAuthority ? [params.queueAuthority] : []
);
}
static getGranteePermissions(
grantee: AccountInfo<Buffer>
): types.SwitchboardPermissionKind {
if (grantee.data.byteLength < ACCOUNT_DISCRIMINATOR_SIZE) {
throw new Error(`Cannot assign permissions to grantee`);
}
const discriminator = grantee.data.slice(0, ACCOUNT_DISCRIMINATOR_SIZE);
// check oracle
if (types.OracleAccountData.discriminator.compare(discriminator) === 0) {
return new PermitOracleHeartbeat();
}
// check aggregator and buffer relayer
if (
types.AggregatorAccountData.discriminator.compare(discriminator) === 0 ||
types.BufferRelayerAccountData.discriminator.compare(discriminator)
) {
return new PermitOracleQueueUsage();
}
// check vrf
if (types.VrfAccountData.discriminator.compare(discriminator) === 0) {
return new PermitVrfRequests();
}
throw new Error(
`Cannot find permissions to assign for account with discriminator of [${discriminator.join(
', '
)}]`
);
}
}

View File

@ -296,7 +296,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
payer: PublicKey,
params: OracleInitParams &
OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>> & {
Partial<PermissionSetParams> & {
queueAuthorityPubkey?: PublicKey;
}
): Promise<[OracleAccount, Array<TransactionObject>]> {
@ -355,9 +355,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
* ```
*/
public async createOracle(
params: OracleInitParams &
OracleStakeParams &
Partial<Omit<PermissionSetParams, 'permission'>>
params: OracleInitParams & OracleStakeParams & Partial<PermissionSetParams>
): Promise<[OracleAccount, Array<TransactionSignature>]> {
const signers: Keypair[] = [];
@ -438,7 +436,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
fundAmount?: number;
funderAuthority?: Keypair;
funderTokenAccount?: PublicKey;
} & Partial<Omit<PermissionSetParams, 'permission'>> & {
} & Partial<PermissionSetParams> & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
} & {
@ -512,7 +510,8 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
funderAuthority: params.funderAuthority,
aggregatorAccount: aggregatorAccount,
queueAccount: this,
jobAuthorities: [],
jobAuthorities: [], // create lease before adding jobs to skip this step
jobPubkeys: [],
})
)[1];
txns.push(leaseInit);
@ -626,7 +625,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
fundAmount?: number;
funderAuthority?: Keypair;
funderTokenAccount?: PublicKey;
} & Partial<Omit<PermissionSetParams, 'permission'>> & {
} & Partial<PermissionSetParams> & {
// job params
jobs?: Array<{ pubkey: PublicKey; weight?: number } | JobInitParams>;
}
@ -745,8 +744,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
*/
public async createVrfInstructions(
payer: PublicKey,
params: Omit<VrfInitParams, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>>
params: Omit<VrfInitParams, 'queueAccount'> & Partial<PermissionSetParams>
): Promise<[VrfAccount, TransactionObject]> {
const queue = await this.loadData();
@ -808,8 +806,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
* ```
*/
public async createVrf(
params: Omit<VrfInitParams, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>>
params: Omit<VrfInitParams, 'queueAccount'> & Partial<PermissionSetParams>
): Promise<[VrfAccount, TransactionSignature]> {
const [vrfAccount, txn] = await this.createVrfInstructions(
this.program.walletPubkey,
@ -845,7 +842,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
public async createBufferRelayerInstructions(
payer: PublicKey,
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>> & {
Partial<PermissionSetParams> & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
@ -947,7 +944,7 @@ export class QueueAccount extends Account<types.OracleQueueAccountData> {
*/
public async createBufferRelayer(
params: Omit<Omit<BufferRelayerInit, 'jobAccount'>, 'queueAccount'> &
Partial<Omit<PermissionSetParams, 'permission'>> & {
Partial<PermissionSetParams> & {
// job params
job: JobAccount | PublicKey | Omit<JobInitParams, 'weight'>;
}
@ -1166,7 +1163,7 @@ export interface QueueInitParams {
*/
name?: string;
/**
* Buffer for queue metadata
* Metadata for the queue for easier identification.
*/
metadata?: string;
/**
@ -1233,18 +1230,58 @@ export interface QueueInitParams {
dataBufferKeypair?: Keypair;
}
export type QueueSetConfigParams = Partial<{
export interface QueueSetConfigParams {
/** Alternative keypair that is the queue authority and is permitted to make account changes. Defaults to the payer if not provided. */
authority?: anchor.web3.Keypair;
name: string;
metadata: string;
unpermissionedFeedsEnabled: boolean;
unpermissionedVrfEnabled: boolean;
enableBufferRelayers: boolean;
slashingEnabled: boolean;
varianceToleranceMultiplier: number;
oracleTimeout: number;
reward: number;
minStake: number;
consecutiveFeedFailureLimit: number;
consecutiveOracleFailureLimit: number;
}>;
/**
* A name to assign to this {@linkcode QueueAccount}
*/
name?: string;
/**
* Metadata for the queue for easier identification.
*/
metadata?: string;
/**
* Enabling this setting means data feeds do not need explicit permission to join the queue.
*/
unpermissionedFeedsEnabled?: boolean;
/**
* Enabling this setting means data feeds do not need explicit permission
* to request VRF proofs and verifications from this queue.
*/
unpermissionedVrfEnabled?: boolean;
/**
* Enabling this setting will allow buffer relayer accounts to call openRound.
*/
enableBufferRelayers?: boolean;
/**
* Whether slashing is enabled on this queue.
*/
slashingEnabled?: boolean;
/**
* The tolerated variance amount oracle results can have from the accepted round result
* before being slashed.
* slashBound = varianceToleranceMultiplier * stdDeviation
*/
varianceToleranceMultiplier?: number;
/**
* Time period (in seconds) we should remove an oracle after if no response.
*/
oracleTimeout?: number;
/**
* Rewards to provide oracles and round openers on this queue.
*/
reward?: number;
/**
* The minimum amount of stake oracles must present to remain on the queue.
*/
minStake?: number;
/**
* Consecutive failure limit for a feed before feed permission is revoked.
*/
consecutiveFeedFailureLimit?: number;
/**
* Consecutive failure limit for an oracle before oracle permission is revoked.
*/
consecutiveOracleFailureLimit?: number;
}

View File

@ -109,6 +109,11 @@ export class TransactionObject implements ITransactionObject {
throw new errors.SwitchboardProgramReadOnlyError();
}
// if empty object, return
if (ixns.length === 0) {
return;
}
// verify num ixns
if (ixns.length > 10) {
throw new errors.TransactionInstructionOverflowError(ixns.length);

View File

@ -8,9 +8,12 @@ import {
AggregatorAccount,
JobAccount,
LeaseAccount,
PermissionAccount,
QueueAccount,
} from '../src';
import { OracleJob } from '@switchboard-xyz/common';
import { assert } from 'console';
import { PermitOracleQueueUsage } from '../src/generated/types/SwitchboardPermission';
describe('Aggregator Tests', () => {
let ctx: TestContext;
@ -248,4 +251,86 @@ describe('Aggregator Tests', () => {
);
}
});
it('Transfers aggregator to a new queue', async () => {
const newQueueAuthority = Keypair.generate();
const [newQueue] = await sbv2.QueueAccount.create(ctx.program, {
name: 'new-queue',
metadata: '',
authority: newQueueAuthority.publicKey,
queueSize: 1,
reward: 0,
minStake: 0,
oracleTimeout: 86400,
slashingEnabled: false,
unpermissionedFeeds: true,
unpermissionedVrf: true,
enableBufferRelayers: false,
});
const [aggregatorAccount] = await queueAccount.createFeed({
queueAuthority: queueAuthority,
batchSize: 1,
minRequiredOracleResults: 1,
minRequiredJobResults: 1,
minUpdateDelaySeconds: 60,
fundAmount: 1.25,
enable: true,
jobs: [
{
weight: 1,
data: OracleJob.encodeDelimited(
OracleJob.fromObject({
tasks: [
{
valueTask: {
value: 1,
},
},
],
})
).finish(),
},
],
});
await aggregatorAccount.loadData();
await aggregatorAccount.transfer({
newQueue,
enable: true,
queueAuthority: newQueueAuthority,
});
const aggregator = await aggregatorAccount.loadData();
assert(
aggregator.queuePubkey.equals(newQueue.publicKey),
`Aggregator queue mismatch, expected ${newQueue.publicKey}, received ${aggregator.queuePubkey}`
);
const [newLeaseAccount] = LeaseAccount.fromSeed(
ctx.program,
newQueue.publicKey,
aggregatorAccount.publicKey
);
const balance = await newLeaseAccount.getBalance();
assert(
balance === 1.25,
`Aggregator did not transfer full lease balance, expected 1.25, received ${balance}`
);
const [newPermissionAccount] = PermissionAccount.fromSeed(
ctx.program,
newQueueAuthority.publicKey,
newQueue.publicKey,
aggregatorAccount.publicKey
);
const newPermission = await newPermissionAccount.loadData();
assert(
newPermission.permissions === PermitOracleQueueUsage.discriminator + 1,
`Aggregator permission mismatch, expected ${
PermitOracleQueueUsage.discriminator + 1
}, received ${newPermission.permissions}`
);
});
});