diff --git a/cli/src/commands/vrf/create/index.ts b/cli/src/commands/vrf/create/index.ts index 0a7697a..3e1b67f 100644 --- a/cli/src/commands/vrf/create/index.ts +++ b/cli/src/commands/vrf/create/index.ts @@ -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,28 +82,35 @@ export default class VrfCreate extends BaseCommand { verifyProgramHasPayer(this.program); const payerKeypair = programWallet(this.program); - 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 = { - programId: new PublicKey(flags.callbackPid), - accounts: flags.accountMeta.map((a) => { - const parsedObject: { - pubkey: string; - isSigner: boolean; - isWritable: boolean; - } = JSON.parse(a); - return { - pubkey: new PublicKey(parsedObject.pubkey), - isSigner: Boolean(parsedObject.isSigner), - isWritable: Boolean(parsedObject.isWritable), - }; - }), - ixData: Buffer.from(ixData), - }; + 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)); + callback = { + programId: new PublicKey(flags.callbackPid), + accounts: flags.accountMeta.map((a) => { + const parsedObject: { + pubkey: string; + isSigner: boolean; + isWritable: boolean; + } = JSON.parse(a); + return { + pubkey: new PublicKey(parsedObject.pubkey), + isSigner: Boolean(parsedObject.isSigner), + isWritable: Boolean(parsedObject.isWritable), + }; + }), + ixData: Buffer.from(ixData), + }; + } else { + throw new Error(`No callback provided`); + } // load VRF params const vrfSecret = flags.vrfKeypair diff --git a/cli/src/commands/vrf/verify.ts b/cli/src/commands/vrf/verify.ts new file mode 100644 index 0000000..584ed71 --- /dev/null +++ b/cli/src/commands/vrf/verify.ts @@ -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 + )}` + ); + } +} diff --git a/cli/src/commands/watch/aggregator.ts b/cli/src/commands/watch/aggregator.ts index bf95683..5e47e37 100644 --- a/cli/src/commands/watch/aggregator.ts +++ b/cli/src/commands/watch/aggregator.ts @@ -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"); +} diff --git a/cli/src/commands/watch/vrf.ts b/cli/src/commands/watch/vrf.ts index 2dce4af..90fdd9f 100644 --- a/cli/src/commands/watch/vrf.ts +++ b/cli/src/commands/watch/vrf.ts @@ -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, context: Context) => { - const vrf = accountCoder.decode("VrfAccountData", accountInfo.data); - printVrf(vrf); - } - ); + 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"); } } diff --git a/libraries/ts/src/sbv2.ts b/libraries/ts/src/sbv2.ts index dea4347..ae59264 100644 --- a/libraries/ts/src/sbv2.ts +++ b/libraries/ts/src/sbv2.ts @@ -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 +): 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 ): 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 + ): Promise { + 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> { 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 ): 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. diff --git a/package.json b/package.json index 7065d5c..675e45a 100644 --- a/package.json +++ b/package.json @@ -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",