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, SystemProgram,
Transaction, Transaction,
} from "@solana/web3.js"; } from "@solana/web3.js";
import { prettyPrintVrf } from "@switchboard-xyz/sbv2-utils"; import {
prettyPrintVrf,
sleep,
verifyProgramHasPayer,
} from "@switchboard-xyz/sbv2-utils";
import { import {
Callback, Callback,
OracleQueueAccount, OracleQueueAccount,
@ -16,8 +20,9 @@ import {
programWallet, programWallet,
VrfAccount, VrfAccount,
} from "@switchboard-xyz/switchboard-v2"; } from "@switchboard-xyz/switchboard-v2";
import fs from "fs";
import BaseCommand from "../../../BaseCommand"; import BaseCommand from "../../../BaseCommand";
import { loadKeypair, sleep, verifyProgramHasPayer } from "../../../utils"; import { loadKeypair } from "../../../utils";
export default class VrfCreate extends BaseCommand { export default class VrfCreate extends BaseCommand {
static description = "create a Switchboard VRF Account"; static description = "create a Switchboard VRF Account";
@ -36,19 +41,25 @@ export default class VrfCreate extends BaseCommand {
queueAuthority: Flags.string({ queueAuthority: Flags.string({
description: "alternative keypair to use for queue authority", description: "alternative keypair to use for queue authority",
}), }),
callback: Flags.string({
description: "filesystem path to callback json",
exclusive: ["accountMeta", "callbackPid", "ixData"],
}),
accountMeta: Flags.string({ accountMeta: Flags.string({
char: "a",
description: "account metas for VRF callback", description: "account metas for VRF callback",
multiple: true, multiple: true,
required: true, exclusive: ["callback"],
dependsOn: ["callbackPid", "ixData"],
}), }),
callbackPid: Flags.string({ callbackPid: Flags.string({
description: "callback program ID", description: "callback program ID",
required: true, exclusive: ["callback"],
dependsOn: ["accountMeta", "ixData"],
}), }),
ixData: Flags.string({ ixData: Flags.string({
description: "instruction data", description: "serialized instruction data in bytes",
required: true, exclusive: ["callback"],
dependsOn: ["accountMeta", "callbackPid"],
}), }),
}; };
@ -63,6 +74,7 @@ export default class VrfCreate extends BaseCommand {
static examples = [ 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 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 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() { async run() {
@ -70,28 +82,35 @@ export default class VrfCreate extends BaseCommand {
verifyProgramHasPayer(this.program); verifyProgramHasPayer(this.program);
const payerKeypair = programWallet(this.program); const payerKeypair = programWallet(this.program);
const ixDataString = let callback: Callback;
flags.ixData.startsWith("[") && flags.ixData.endsWith("]") if (flags.callback) {
? flags.ixData.slice(1, -1) callback = JSON.parse(fs.readFileSync(flags.callback, "utf8"));
: flags.ixData; } else if (flags.callbackPid && flags.accountMeta && flags.ixData) {
const ixDataArray = ixDataString.split(","); const ixDataString =
const ixData = ixDataArray.map((n) => Number.parseInt(n, 10)); flags.ixData.startsWith("[") && flags.ixData.endsWith("]")
const callback: Callback = { ? flags.ixData.slice(1, -1)
programId: new PublicKey(flags.callbackPid), : flags.ixData;
accounts: flags.accountMeta.map((a) => { const ixDataArray = ixDataString.split(",");
const parsedObject: { const ixData = ixDataArray.map((n) => Number.parseInt(n, 10));
pubkey: string; callback = {
isSigner: boolean; programId: new PublicKey(flags.callbackPid),
isWritable: boolean; accounts: flags.accountMeta.map((a) => {
} = JSON.parse(a); const parsedObject: {
return { pubkey: string;
pubkey: new PublicKey(parsedObject.pubkey), isSigner: boolean;
isSigner: Boolean(parsedObject.isSigner), isWritable: boolean;
isWritable: Boolean(parsedObject.isWritable), } = JSON.parse(a);
}; return {
}), pubkey: new PublicKey(parsedObject.pubkey),
ixData: Buffer.from(ixData), isSigner: Boolean(parsedObject.isSigner),
}; isWritable: Boolean(parsedObject.isWritable),
};
}),
ixData: Buffer.from(ixData),
};
} else {
throw new Error(`No callback provided`);
}
// load VRF params // load VRF params
const vrfSecret = flags.vrfKeypair 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" ) + "\r\n"
) )
); );
const ws = this.program.addEventListener(
"AggregatorValueUpdateEvent", printAggregator(aggregator);
(event, slot) => {
if (aggregatorAccount.publicKey.equals(event.feedPubkey)) { const ws = aggregatorAccount.onChange((aggregator) => {
const decimal = SwitchboardDecimal.from(event.value); printAggregator(aggregator);
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");
}
}
);
} }
async catch(error) { async catch(error) {
super.catch(error, "failed to watch aggregator"); 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 * 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 { chalkString } from "@switchboard-xyz/sbv2-utils";
import { VrfAccount } from "@switchboard-xyz/switchboard-v2"; import { VrfAccount } from "@switchboard-xyz/switchboard-v2";
import chalk from "chalk"; import chalk from "chalk";
@ -39,19 +39,13 @@ export default class WatchVrf extends BaseCommand {
); );
printVrf(vrfData); printVrf(vrfData);
const accountCoder = new anchor.BorshAccountsCoder(this.program.idl); const watchWs = vrfAccount.onChange((vrf) => {
printVrf(vrf);
const watchWs = this.program.provider.connection.onAccountChange( });
vrfAccount.publicKey,
(accountInfo: AccountInfo<Buffer>, context: Context) => {
const vrf = accountCoder.decode("VrfAccountData", accountInfo.data);
printVrf(vrf);
}
);
} }
async catch(error) { 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); 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. * Switchboard precisioned representation of numbers.
*/ */
@ -238,6 +285,8 @@ export interface VaultTransferParams {
* Account type representing Switchboard global program state. * Account type representing Switchboard global program state.
*/ */
export class ProgramStateAccount { export class ProgramStateAccount {
static accountName = "SbState";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -678,9 +727,11 @@ export interface AggregatorSetUpdateIntervalParams {
* Account type representing an aggregator (data feed). * Account type representing an aggregator (data feed).
*/ */
export class AggregatorAccount { export class AggregatorAccount {
static accountName = "AggregatorAccountData";
program: anchor.Program; program: anchor.Program;
publicKey?: PublicKey; publicKey: PublicKey; // why was this optional
keypair?: Keypair; keypair?: Keypair;
@ -711,8 +762,10 @@ export class AggregatorAccount {
accountInfo: AccountInfo<Buffer> accountInfo: AccountInfo<Buffer>
): any { ): any {
const coder = new anchor.BorshAccountsCoder(program.idl); const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "AggregatorAccountData"; const aggregator = coder.decode(
const aggregator = coder.decode(key, accountInfo?.data!); AggregatorAccount.accountName,
accountInfo?.data!
);
return aggregator; return aggregator;
} }
@ -738,6 +791,49 @@ export class AggregatorAccount {
return aggregator; 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>> { async loadHistory(aggregator?: any): Promise<Array<AggregatorHistoryRow>> {
aggregator = aggregator ?? (await this.loadData()); aggregator = aggregator ?? (await this.loadData());
if (aggregator.historyBuffer == PublicKey.default) { if (aggregator.historyBuffer == PublicKey.default) {
@ -930,7 +1026,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs."); throw new Error("Failed to load feed jobs.");
} }
const jobs = jobAccountDatas.map((item) => { const jobs = jobAccountDatas.map((item) => {
return coder.decode("JobAccountData", item.account.data); return coder.decode(JobAccount.accountName, item.account.data);
}); });
return jobs; return jobs;
} }
@ -952,7 +1048,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs."); throw new Error("Failed to load feed jobs.");
} }
const jobs = jobAccountDatas.map((item) => { 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 protos.OracleJob.decodeDelimited(decoded.data);
}); });
return jobs; return jobs;
@ -971,7 +1067,7 @@ export class AggregatorAccount {
throw new Error("Failed to load feed jobs."); throw new Error("Failed to load feed jobs.");
} }
const jobs = jobAccountDatas.map((item) => { 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 decoded.hash;
}); });
return jobs; return jobs;
@ -1498,6 +1594,8 @@ export interface JobInitParams {
* a protocol buffer. * a protocol buffer.
*/ */
export class JobAccount { export class JobAccount {
static accountName = "JobAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -1602,8 +1700,7 @@ export class JobAccount {
accountInfo: AccountInfo<Buffer> accountInfo: AccountInfo<Buffer>
): any { ): any {
const coder = new anchor.BorshAccountsCoder(program.idl); const coder = new anchor.BorshAccountsCoder(program.idl);
const key = "JobAccountData"; const data = coder.decode(JobAccount.accountName, accountInfo?.data!);
const data = coder.decode(key, accountInfo?.data!);
return data; return data;
} }
@ -1679,6 +1776,8 @@ export enum SwitchboardPermissionValue {
* account signer to another account. * account signer to another account.
*/ */
export class PermissionAccount { export class PermissionAccount {
static accountName = "PermissionAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -2025,6 +2124,8 @@ export interface OracleQueueSetVrfSettingsParams {
* permitted data feeds. * permitted data feeds.
*/ */
export class OracleQueueAccount { export class OracleQueueAccount {
static accountName = "OracleQueueAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -2699,6 +2800,8 @@ export class CrankRow {
* A Switchboard account representing a crank of aggregators ordered by next update time. * A Switchboard account representing a crank of aggregators ordered by next update time.
*/ */
export class CrankAccount { export class CrankAccount {
static accountName = "CrankAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -3077,6 +3180,8 @@ export interface OracleWithdrawParams {
* and escrow account. * and escrow account.
*/ */
export class OracleAccount { export class OracleAccount {
static accountName = "OracleAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -3428,6 +3533,8 @@ export interface VrfProveParams {
* A Switchboard VRF account. * A Switchboard VRF account.
*/ */
export class VrfAccount { export class VrfAccount {
static accountName = "VrfAccountData";
program: anchor.Program; program: anchor.Program;
publicKey: PublicKey; publicKey: PublicKey;
@ -3470,6 +3577,17 @@ export class VrfAccount {
return vrf; 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. * Get the size of a VrfAccount on chain.
* @return size. * @return size.

View File

@ -14,6 +14,8 @@
], ],
"scripts": { "scripts": {
"watch": "yarn workspace website start & yarn workspaces run watch", "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", "start": "echo \"Error: no start script specified\" && exit 1",
"anchor:setup": "anchor build && node ./tools/scripts/setup-example-programs.js", "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", "test:anchor": "yarn workspace anchor-feed-parser anchor:test && yarn workspace spl-feed-parser anchor:test && yarn workspace anchor-vrf-parser anchor:test",