solana.js: added buffer-relayer tests

This commit is contained in:
Conner Gallagher 2023-01-03 13:37:43 -07:00
parent 9f3a99db5e
commit a198db022d
3 changed files with 333 additions and 54 deletions

View File

@ -18,12 +18,17 @@ import { BN } from 'bn.js';
import * as errors from '../errors';
import * as types from '../generated';
import { SwitchboardProgram } from '../SwitchboardProgram';
import { TransactionObject } from '../TransactionObject';
import {
TransactionObject,
TransactionObjectOptions,
} from '../TransactionObject';
import { Account, OnAccountChangeCallback } from './account';
import { JobAccount } from './jobAccount';
import { PermissionAccount } from './permissionAccount';
import { QueueAccount } from './queueAccount';
import { promiseWithTimeout } from '@switchboard-xyz/common';
import { OracleAccount } from './oracleAccount';
import { bufferRelayerSaveResult } from '../generated';
/**
* Account type holding a buffer of data sourced from the buffers sole {@linkcode JobAccount}. A buffer relayer has no consensus mechanism and relies on trusting an {@linkcode OracleAccount} to respond honestly. A buffer relayer has a max capacity of 500 bytes.
@ -186,27 +191,44 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
public async openRoundInstructions(
payer: PublicKey,
params: BufferRelayerOpenRoundParams
params?: BufferRelayerOpenRoundParams
): Promise<TransactionObject> {
const ixns: TransactionInstruction[] = [];
const bufferRelayer = params.bufferRelayer ?? (await this.loadData());
const txns: TransactionObject[] = [];
const bufferRelayer = params?.bufferRelayer ?? (await this.loadData());
const queueAccount =
params.queueAccount ??
params?.queueAccount ??
new QueueAccount(this.program, bufferRelayer.queuePubkey);
const queue = params.queue ?? (await queueAccount.loadData());
const queue = params?.queue ?? (await queueAccount.loadData());
const openRoundAmount = this.program.mint.fromTokenAmountBN(queue.reward);
let tokenWallet: PublicKey;
if (params?.tokenWallet) {
tokenWallet = params.tokenWallet;
// check if we need to wrap any funds
const tokenAccount = await getAccount(
this.program.connection,
params.tokenWallet
tokenWallet
);
const tokenAmountBN = new BN(tokenAccount.amount.toString());
if (tokenAmountBN.lt(queue.reward)) {
const wrapTxn = await this.program.mint.wrapInstructions(payer, {
fundUpTo: this.program.mint.fromTokenAmountBN(queue.reward),
fundUpTo: openRoundAmount,
});
ixns.push(...wrapTxn.ixns);
txns.push(wrapTxn);
}
} else {
const [userTokenWallet, txn] =
await this.program.mint.getOrCreateWrappedUserInstructions(payer, {
fundUpTo: openRoundAmount,
});
tokenWallet = userTokenWallet;
if (txn !== undefined) {
txns.push(txn);
}
}
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
queue.authority,
@ -214,9 +236,11 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
this.publicKey
);
ixns.push(
const openRoundTxn = new TransactionObject(
payer,
[
createTransferInstruction(
params.tokenWallet,
tokenWallet,
bufferRelayer.escrow,
payer,
BigInt(queue.reward.toString())
@ -237,14 +261,22 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
escrow: bufferRelayer.escrow,
programState: this.program.programState.publicKey,
}
)
),
],
[]
);
txns.push(openRoundTxn);
return new TransactionObject(payer, ixns, []);
const packed = TransactionObject.pack(txns);
if (packed.length > 1) {
throw new Error(`Failed to pack instructions into a single txn`);
}
return packed[0];
}
public async openRound(
params: BufferRelayerOpenRoundParams
params?: BufferRelayerOpenRoundParams
): Promise<TransactionSignature> {
const openRound = await this.openRoundInstructions(
this.program.walletPubkey,
@ -303,15 +335,109 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
return [state, openRoundSignature];
}
public getAccounts(params: {
queueAccount: QueueAccount;
queueAuthority: PublicKey;
}) {
const queueAccount = params.queueAccount;
public async saveResultInstructions(
payer: PublicKey,
params: BufferRelayerSaveResultParams,
options?: TransactionObjectOptions
): Promise<TransactionObject> {
const bufferRelayer = await this.loadData();
const [queueAccount, queue] = await QueueAccount.load(
this.program,
bufferRelayer.queuePubkey
);
const { permissionAccount, permissionBump } = this.getAccounts(
queueAccount,
queue.authority
);
const [oracleAccount, oracle] = await OracleAccount.load(
this.program,
bufferRelayer.currentRound.oraclePubkey
);
return this.saveResultSyncInstructions(
payer,
{
...params,
escrow: bufferRelayer.escrow,
queueAccount: queueAccount,
queueAuthority: queue.authority,
queueDataBuffer: queue.dataBuffer,
oracleAccount: oracleAccount,
oracleAuthority: oracle.oracleAuthority,
oracleTokenAccount: oracle.tokenAccount,
permissionAccount: permissionAccount,
permissionBump: permissionBump,
},
options
);
}
public async saveResult(
params: BufferRelayerSaveResultParams,
options?: TransactionObjectOptions
): Promise<TransactionSignature> {
const saveResult = await this.saveResultInstructions(
this.program.walletPubkey,
params,
options
);
const txnSignature = await this.program.signAndSend(saveResult);
return txnSignature;
}
public async saveResultSyncInstructions(
payer: PublicKey,
params: BufferRelayerSaveResultSyncParams,
options?: TransactionObjectOptions
): Promise<TransactionObject> {
const saveResultIxn = bufferRelayerSaveResult(
this.program,
{
params: {
stateBump: this.program.programState.bump,
permissionBump: params.permissionBump,
result: params.result,
success: params.success,
},
},
{
bufferRelayer: this.publicKey,
oracleAuthority: params.oracleAuthority,
oracle: params.oracleAccount.publicKey,
oracleQueue: params.queueAccount.publicKey,
dataBuffer: params.queueDataBuffer,
queueAuthority: params.queueAuthority,
permission: params.permissionAccount.publicKey,
escrow: params.escrow,
programState: this.program.programState.publicKey,
oracleWallet: params.oracleTokenAccount,
tokenProgram: spl.TOKEN_PROGRAM_ID,
}
);
return new TransactionObject(payer, [saveResultIxn], [], options);
}
public async saveResultSync(
params: BufferRelayerSaveResultSyncParams,
options?: TransactionObjectOptions
): Promise<TransactionSignature> {
const saveResult = await this.saveResultSyncInstructions(
this.program.walletPubkey,
params,
options
);
const txnSignature = await this.program.signAndSend(saveResult);
return txnSignature;
}
public getAccounts(queueAccount: QueueAccount, queueAuthority: PublicKey) {
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
this.program,
params.queueAuthority,
queueAuthority,
queueAccount.publicKey,
this.publicKey
);
@ -359,10 +485,10 @@ export class BufferRelayerAccount extends Account<types.BufferRelayerAccountData
new QueueAccount(this.program, bufferRelayer.queuePubkey);
const queue = _queue ?? (await queueAccount.loadData());
const { permissionAccount, permissionBump } = this.getAccounts({
const { permissionAccount, permissionBump } = this.getAccounts(
queueAccount,
queueAuthority: queue.authority,
});
queue.authority
);
const permission = await permissionAccount.loadData();
const bufferEscrow = await this.program.mint.getAccount(
@ -439,8 +565,26 @@ export type BufferRelayerAccountsJSON = types.BufferRelayerAccountDataJSON & {
};
export type BufferRelayerOpenRoundParams = {
tokenWallet: PublicKey;
tokenWallet?: PublicKey;
bufferRelayer?: types.BufferRelayerAccountData;
queueAccount?: QueueAccount;
queue?: types.OracleQueueAccountData;
};
export type BufferRelayerSaveResultParams = {
result: Buffer;
success: boolean;
};
export type BufferRelayerSaveResultSyncParams =
BufferRelayerSaveResultParams & {
escrow: PublicKey;
queueAccount: QueueAccount;
queueAuthority: PublicKey;
queueDataBuffer: PublicKey;
oracleAccount: OracleAccount;
oracleAuthority: PublicKey;
oracleTokenAccount: PublicKey;
permissionAccount: PermissionAccount;
permissionBump: number;
};

View File

@ -32,7 +32,7 @@ export class QueueJson implements IQueueInitParams {
// accounts
authority?: Keypair;
keypair: Keypair;
dataBuffer: Keypair;
dataBufferKeypair: Keypair;
constructor(object: Record<string, any>) {
this.name = parseString(object, 'name', '');
@ -82,7 +82,7 @@ export class QueueJson implements IQueueInitParams {
this.authority = authorityPath ? loadKeypair(authorityPath) : undefined;
const dataBufferPath = parseString(object, 'dataBufferKeypair');
this.dataBuffer = dataBufferPath
this.dataBufferKeypair = dataBufferPath
? loadKeypair(dataBufferPath)
: Keypair.generate();
}
@ -105,7 +105,7 @@ export class QueueJson implements IQueueInitParams {
enableBufferRelayers: this.enableBufferRelayers,
authority: this.authority ? keypairToString(this.authority) : undefined,
keypair: keypairToString(this.keypair),
dataBuffer: keypairToString(this.dataBuffer),
dataBufferKeypair: keypairToString(this.dataBufferKeypair),
};
}
}

View File

@ -0,0 +1,135 @@
import 'mocha';
import assert from 'assert';
import { setupTest, TestContext } from './utilts';
import { Keypair, PublicKey } from '@solana/web3.js';
import {
BufferRelayerAccount,
OracleAccount,
QueueAccount,
types,
} from '../src';
import { OracleJob } from '@switchboard-xyz/common';
describe('BufferRelayer Tests', () => {
let ctx: TestContext;
const queueAuthority = Keypair.generate();
let queueAccount: QueueAccount;
let oracleAccount: OracleAccount;
let oracle: types.OracleAccountData;
let bufferAccount: BufferRelayerAccount;
const expectedResult: Buffer = Buffer.from(
JSON.stringify({
userId: 1,
id: 1,
title: 'delectus aut autem',
completed: false,
}),
'utf-8'
);
let userTokenAddress: PublicKey;
before(async () => {
ctx = await setupTest();
[queueAccount] = await QueueAccount.create(ctx.program, {
name: 'buffer-relayer-queue',
metadata: '',
authority: queueAuthority.publicKey,
queueSize: 1,
reward: 0,
minStake: 0,
oracleTimeout: 86400,
slashingEnabled: false,
unpermissionedFeeds: true,
unpermissionedVrf: true,
enableBufferRelayers: true,
});
[oracleAccount] = await queueAccount.createOracle({
name: 'oracle-1',
metadata: 'oracle-1',
queueAuthority,
enable: true,
});
await oracleAccount.heartbeat();
oracle = await oracleAccount.loadData();
assert(
oracle.oracleAuthority.equals(ctx.payer.publicKey),
'Incorrect oracle authority'
);
[userTokenAddress] = await ctx.program.mint.getOrCreateWrappedUser(
ctx.payer.publicKey,
{ fundUpTo: 0.1 }
);
});
it('Creates a Buffer Relayer', async () => {
[bufferAccount] = await queueAccount.createBufferRelayer({
name: 'My Buffer',
minUpdateDelaySeconds: 30,
enable: true,
queueAuthorityPubkey: queueAuthority.publicKey,
queueAuthority: queueAuthority,
job: {
name: 'Buffer Job',
data: Buffer.from(
OracleJob.encodeDelimited(
OracleJob.create({
tasks: [
OracleJob.Task.create({
httpTask: OracleJob.HttpTask.create({
url: 'https://jsonplaceholder.typicode.com/todos/1',
}),
}),
],
})
).finish()
),
},
});
});
it('Calls openRound on a BufferRelayer', async () => {
if (!bufferAccount) {
throw new Error(`No BufferRelayer account`);
}
await bufferAccount.openRound({
tokenWallet: userTokenAddress,
});
const bufferRelayer = await bufferAccount.loadData();
assert(
bufferRelayer.currentRound.oraclePubkey.equals(oracleAccount.publicKey),
`Oracle assignment mismatch, expected ${oracleAccount.publicKey}, received ${bufferRelayer.currentRound.oraclePubkey}`
);
});
it('Calls saveResult on a BufferRelayer', async () => {
if (!bufferAccount) {
throw new Error(`No BufferRelayer account`);
}
await bufferAccount.saveResult({
result: expectedResult,
success: true,
});
const bufferRelayer = await bufferAccount.loadData();
assert(
Buffer.compare(expectedResult, bufferRelayer.result) === 0,
`BufferRelayer result mismatch, expected [${new Uint8Array(
expectedResult
)}], received [${new Uint8Array(bufferRelayer.result)}]`
);
});
});