added aggregator onChange and onResult events

added watchSwitchboardAccount fn


cleanup


cleanup


added vrf verify command


vrf cli
This commit is contained in:
Conner Gallagher 2022-06-20 07:52:12 -05:00
parent f984813d17
commit 2e8479535e
6 changed files with 263 additions and 61 deletions

View File

@ -7,7 +7,11 @@ import {
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { prettyPrintVrf } from "@switchboard-xyz/sbv2-utils";
import {
prettyPrintVrf,
sleep,
verifyProgramHasPayer,
} from "@switchboard-xyz/sbv2-utils";
import {
Callback,
OracleQueueAccount,
@ -16,8 +20,9 @@ import {
programWallet,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
import fs from "fs";
import BaseCommand from "../../../BaseCommand";
import { loadKeypair, sleep, verifyProgramHasPayer } from "../../../utils";
import { loadKeypair } from "../../../utils";
export default class VrfCreate extends BaseCommand {
static description = "create a Switchboard VRF Account";
@ -36,19 +41,25 @@ export default class VrfCreate extends BaseCommand {
queueAuthority: Flags.string({
description: "alternative keypair to use for queue authority",
}),
callback: Flags.string({
description: "filesystem path to callback json",
exclusive: ["accountMeta", "callbackPid", "ixData"],
}),
accountMeta: Flags.string({
char: "a",
description: "account metas for VRF callback",
multiple: true,
required: true,
exclusive: ["callback"],
dependsOn: ["callbackPid", "ixData"],
}),
callbackPid: Flags.string({
description: "callback program ID",
required: true,
exclusive: ["callback"],
dependsOn: ["accountMeta", "ixData"],
}),
ixData: Flags.string({
description: "instruction data",
required: true,
description: "serialized instruction data in bytes",
exclusive: ["callback"],
dependsOn: ["accountMeta", "callbackPid"],
}),
};
@ -63,6 +74,7 @@ export default class VrfCreate extends BaseCommand {
static examples = [
'sbv2 vrf:create 9WZ59yz95bd3XwJxDPVE2PjvVWmSy9WM1NgGD2Hqsohw --keypair ../payer-keypair.json -v --enable --queueAuthority queue-authority-keypair.json --callbackPid 6MLk7G54uHZ7JuzNxpBAVENANrgM9BZ51pKkzGwPYBCE --ixData "[145,72,9,94,61,97,126,106]" -a "{"pubkey": "HpQoFL5kxPp2JCFvjsVTvBd7navx4THLefUU68SXAyd6","isSigner": false,"isWritable": true}" -a "{"pubkey": "8VdBtS8ufkXMCa6Yr9E4KVCfX2inVZVwU4KGg2CL1q7P","isSigner": false,"isWritable": false}"',
'sbv2 vrf:create 9WZ59yz95bd3XwJxDPVE2PjvVWmSy9WM1NgGD2Hqsohw --keypair ../payer-keypair.json -v --enable --queueAuthority oracle-keypair.json --callbackPid 6MLk7G54uHZ7JuzNxpBAVENANrgM9BZ51pKkzGwPYBCE --ixData "[145,72,9,94,61,97,126,106]" -a "{"pubkey": "HYKi1grticLXPe5vqapUHhm976brwqRob8vqRnWMKWL5","isSigner": false,"isWritable": true}" -a "{"pubkey": "6vG9QLMgSvsfjvSpDxWfZ2MGPYGzEYoBxviLG7cr4go","isSigner": false,"isWritable": false}"',
"sbv2 vrf:create 9WZ59yz95bd3XwJxDPVE2PjvVWmSy9WM1NgGD2Hqsohw --keypair ../payer-keypair.json -v --enable --queueAuthority queue-authority-keypair.json --callback callback-example.json",
];
async run() {
@ -70,13 +82,17 @@ export default class VrfCreate extends BaseCommand {
verifyProgramHasPayer(this.program);
const payerKeypair = programWallet(this.program);
let callback: Callback;
if (flags.callback) {
callback = JSON.parse(fs.readFileSync(flags.callback, "utf8"));
} else if (flags.callbackPid && flags.accountMeta && flags.ixData) {
const ixDataString =
flags.ixData.startsWith("[") && flags.ixData.endsWith("]")
? flags.ixData.slice(1, -1)
: flags.ixData;
const ixDataArray = ixDataString.split(",");
const ixData = ixDataArray.map((n) => Number.parseInt(n, 10));
const callback: Callback = {
callback = {
programId: new PublicKey(flags.callbackPid),
accounts: flags.accountMeta.map((a) => {
const parsedObject: {
@ -92,6 +108,9 @@ export default class VrfCreate extends BaseCommand {
}),
ixData: Buffer.from(ixData),
};
} else {
throw new Error(`No callback provided`);
}
// load VRF params
const vrfSecret = flags.vrfKeypair

View File

@ -0,0 +1,64 @@
import { PublicKey } from "@solana/web3.js";
import {
toVrfStatusString,
verifyProgramHasPayer,
} from "@switchboard-xyz/sbv2-utils";
import {
OracleAccount,
programWallet,
VrfAccount,
} from "@switchboard-xyz/switchboard-v2";
import BaseCommand from "../../BaseCommand";
export default class VrfVerify extends BaseCommand {
static description = "if ready, verify a VRF proof";
static examples = [];
static flags = {
...BaseCommand.flags,
};
static args = [
{
name: "vrfKey",
description: "public key of the VRF account to request randomness for",
},
];
async run() {
const { args, flags } = await this.parse(VrfVerify);
verifyProgramHasPayer(this.program);
const payerKeypair = programWallet(this.program);
const vrfAccount = new VrfAccount({
program: this.program,
publicKey: new PublicKey(args.vrfKey),
});
const vrf = await vrfAccount.loadData();
const status = toVrfStatusString(vrf.status);
if (status !== "statusVerifying") {
throw new Error(`Vrf not ready to be verified, current status ${status}`);
}
if (vrf.txRemaining === 0) {
throw new Error(`vrf has ${vrf.txRemaining} txRemaining to verify proof`);
}
const oracle = new OracleAccount({
program: this.program,
publicKey: vrf.builders[0].producer as PublicKey,
});
const signatures = await vrfAccount.verify(oracle);
this.log(
`VrfAccount verification instructions sent\r\n${JSON.stringify(
signatures,
undefined,
2
)}`
);
}
}

View File

@ -50,22 +50,27 @@ export default class WatchAggregator extends BaseCommand {
) + "\r\n"
)
);
const ws = this.program.addEventListener(
"AggregatorValueUpdateEvent",
(event, slot) => {
if (aggregatorAccount.publicKey.equals(event.feedPubkey)) {
const decimal = SwitchboardDecimal.from(event.value);
const big = decimal.toBig();
const timestamp = anchorBNtoDateTimeString(event.timestamp);
process.stdout.moveCursor(0, -1); // up one line
process.stdout.clearLine(1); // from cursor to end
process.stdout.write(chalkString(timestamp, big, 30) + "\r\n");
}
}
);
printAggregator(aggregator);
const ws = aggregatorAccount.onChange((aggregator) => {
printAggregator(aggregator);
});
}
async catch(error) {
super.catch(error, "failed to watch aggregator");
}
}
function printAggregator(aggregator: any) {
const result = SwitchboardDecimal.from(
aggregator.latestConfirmedRound.result
).toBig();
const timestamp = anchorBNtoDateTimeString(
aggregator.latestConfirmedRound.roundOpenTimestamp
);
process.stdout.moveCursor(0, -1); // up one line
process.stdout.clearLine(1); // from cursor to end
process.stdout.write(chalkString(timestamp, result, 30) + "\r\n");
}

View File

@ -1,5 +1,5 @@
import * as anchor from "@project-serum/anchor";
import { AccountInfo, Context, PublicKey } from "@solana/web3.js";
import { PublicKey } from "@solana/web3.js";
import { chalkString } from "@switchboard-xyz/sbv2-utils";
import { VrfAccount } from "@switchboard-xyz/switchboard-v2";
import chalk from "chalk";
@ -39,19 +39,13 @@ export default class WatchVrf extends BaseCommand {
);
printVrf(vrfData);
const accountCoder = new anchor.BorshAccountsCoder(this.program.idl);
const watchWs = this.program.provider.connection.onAccountChange(
vrfAccount.publicKey,
(accountInfo: AccountInfo<Buffer>, context: Context) => {
const vrf = accountCoder.decode("VrfAccountData", accountInfo.data);
const watchWs = vrfAccount.onChange((vrf) => {
printVrf(vrf);
}
);
});
}
async catch(error) {
super.catch(error, "failed to watch aggregator");
super.catch(error, "failed to watch vrf account");
}
}

View File

@ -97,6 +97,53 @@ export async function loadSwitchboardProgram(
return new anchor.Program(anchorIdl, programId, provider);
}
// should also check if pubkey is a token account
export const findAccountName = (
program: anchor.Program,
accountInfo: AccountInfo<Buffer>
): string => {
const accountDiscriminator = accountInfo.data.slice(
0,
anchor.ACCOUNT_DISCRIMINATOR_SIZE
);
for (const accountDef of program.idl.accounts) {
const typeDiscriminator = anchor.BorshAccountsCoder.accountDiscriminator(
accountDef.name
);
if (Buffer.compare(accountDiscriminator, typeDiscriminator) === 0) {
return accountDef.name;
}
}
throw new Error("failed to match account type by discriminator");
};
/** Callback to pass deserialized account data when updated on-chain */
export type OnAccountChangeCallback = (accountData: any) => void;
export function watchSwitchboardAccount(
program: anchor.Program,
publicKey: PublicKey,
accountName: string,
callback: OnAccountChangeCallback
): number {
// const accountName = await findAccountName(program, publicKey);
const accountDef = program.idl.accounts.find((a) => a.name === accountName);
if (!accountDef) {
throw new Error(`Failed to find account ${accountName} in switchboard IDL`);
}
const coder = new anchor.BorshAccountsCoder(program.idl);
return program.provider.connection.onAccountChange(
publicKey,
(accountInfo, context) => {
const data = coder.decode(accountName, accountInfo?.data);
callback(data);
}
);
}
/**
* Switchboard precisioned representation of numbers.
*/
@ -238,6 +285,8 @@ export interface VaultTransferParams {
* Account type representing Switchboard global program state.
*/
export class ProgramStateAccount {
static accountName = "SbState";
program: anchor.Program;
publicKey: PublicKey;
@ -678,9 +727,11 @@ export interface AggregatorSetUpdateIntervalParams {
* Account type representing an aggregator (data feed).
*/
export class AggregatorAccount {
static accountName = "AggregatorAccountData";
program: anchor.Program;
publicKey?: PublicKey;
publicKey: PublicKey; // why was this optional
keypair?: Keypair;
@ -711,8 +762,10 @@ export class AggregatorAccount {
accountInfo: AccountInfo<Buffer>
): any {
const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "AggregatorAccountData";
const aggregator = coder.decode(key, accountInfo?.data!);
const aggregator = coder.decode(
AggregatorAccount.accountName,
accountInfo?.data!
);
return aggregator;
}
@ -738,6 +791,49 @@ export class AggregatorAccount {
return aggregator;
}
onChange(callback: OnAccountChangeCallback): number {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
return this.program.provider.connection.onAccountChange(
this.publicKey,
(accountInfo, context) => {
const aggregator = coder.decode(
AggregatorAccount.accountName,
accountInfo?.data
);
callback(aggregator);
}
);
}
async onResult(
callback: (result: {
feedPubkey: PublicKey;
result: Big;
slot: anchor.BN;
timestamp: anchor.BN;
oracleValues: Big[];
}) => Promise<void>
): Promise<number> {
return this.program.addEventListener(
"AggregatorValueUpdateEvent",
(event, slot) => {
const result = SwitchboardDecimal.from(
event.value as { mantissa: anchor.BN; scale: number }
).toBig();
const oracleValues: Big[] = (
event.oracleValues as { mantissa: anchor.BN; scale: number }[]
).map((v) => SwitchboardDecimal.from(v).toBig());
callback({
feedPubkey: event.feedPubkey as PublicKey,
result,
slot: event.slot as anchor.BN,
timestamp: event.timestamp as anchor.BN,
oracleValues,
});
}
);
}
async loadHistory(aggregator?: any): Promise<Array<AggregatorHistoryRow>> {
aggregator = aggregator ?? (await this.loadData());
if (aggregator.historyBuffer == PublicKey.default) {
@ -930,7 +1026,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
return coder.decode("JobAccountData", item.account.data);
return coder.decode(JobAccount.accountName, item.account.data);
});
return jobs;
}
@ -952,7 +1048,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
const decoded = coder.decode("JobAccountData", item.account.data);
const decoded = coder.decode(JobAccount.accountName, item.account.data);
return protos.OracleJob.decodeDelimited(decoded.data);
});
return jobs;
@ -971,7 +1067,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs.");
}
const jobs = jobAccountDatas.map((item) => {
const decoded = coder.decode("JobAccountData", item.account.data);
const decoded = coder.decode(JobAccount.accountName, item.account.data);
return decoded.hash;
});
return jobs;
@ -1498,6 +1594,8 @@ export interface JobInitParams {
* a protocol buffer.
*/
export class JobAccount {
static accountName = "JobAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -1602,8 +1700,7 @@ export class JobAccount {
accountInfo: AccountInfo<Buffer>
): any {
const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "JobAccountData";
const data = coder.decode(key, accountInfo?.data!);
const data = coder.decode(JobAccount.accountName, accountInfo?.data!);
return data;
}
@ -1679,6 +1776,8 @@ export enum SwitchboardPermissionValue {
* account signer to another account.
*/
export class PermissionAccount {
static accountName = "PermissionAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -2025,6 +2124,8 @@ export interface OracleQueueSetVrfSettingsParams {
* permitted data feeds.
*/
export class OracleQueueAccount {
static accountName = "OracleQueueAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -2699,6 +2800,8 @@ export class CrankRow {
* A Switchboard account representing a crank of aggregators ordered by next update time.
*/
export class CrankAccount {
static accountName = "CrankAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -3077,6 +3180,8 @@ export interface OracleWithdrawParams {
* and escrow account.
*/
export class OracleAccount {
static accountName = "OracleAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -3428,6 +3533,8 @@ export interface VrfProveParams {
* A Switchboard VRF account.
*/
export class VrfAccount {
static accountName = "VrfAccountData";
program: anchor.Program;
publicKey: PublicKey;
@ -3470,6 +3577,17 @@ export class VrfAccount {
return vrf;
}
onChange(callback: OnAccountChangeCallback): number {
const coder = new anchor.BorshAccountsCoder(this.program.idl);
return this.program.provider.connection.onAccountChange(
this.publicKey,
(accountInfo, context) => {
const vrf = coder.decode(VrfAccount.accountName, accountInfo?.data);
callback(vrf);
}
);
}
/**
* Get the size of a VrfAccount on chain.
* @return size.

View File

@ -14,6 +14,8 @@
],
"scripts": {
"watch": "yarn workspace website start & yarn workspaces run watch",
"build:ts": "yarn workspace @switchboard-xyz/switchboard-v2 build",
"build:cli": "yarn workspace @switchboard-xyz/switchboardv2-cli build",
"start": "echo \"Error: no start script specified\" && exit 1",
"anchor:setup": "anchor build && node ./tools/scripts/setup-example-programs.js",
"test:anchor": "yarn workspace anchor-feed-parser anchor:test && yarn workspace spl-feed-parser anchor:test && yarn workspace anchor-vrf-parser anchor:test",