From 466e9948bfb67627d5988632ee5d9d437c10d622 Mon Sep 17 00:00:00 2001 From: Steven Luscher Date: Tue, 6 Sep 2022 22:37:43 -0700 Subject: [PATCH] feat: added `authorizeWithSeed` to the vote program in web3.js (#27627) * fix: recursively size variable size structs in your buffer layouts * feat: added `authorizeWithSeed` to the vote program in web3.js --- web3.js/src/layout.ts | 19 ++++ web3.js/src/programs/vote.ts | 109 ++++++++++++++++++++- web3.js/test/program-tests/vote.test.ts | 125 ++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 1 deletion(-) diff --git a/web3.js/src/layout.ts b/web3.js/src/layout.ts index c4d161e04..35a5f6d80 100644 --- a/web3.js/src/layout.ts +++ b/web3.js/src/layout.ts @@ -1,6 +1,8 @@ import {Buffer} from 'buffer'; import * as BufferLayout from '@solana/buffer-layout'; +import {VoteAuthorizeWithSeedArgs} from './programs/vote'; + /** * Layout for a public key */ @@ -141,6 +143,23 @@ export const voteInit = (property: string = 'voteInit') => { ); }; +/** + * Layout for a VoteAuthorizeWithSeedArgs object + */ +export const voteAuthorizeWithSeedArgs = ( + property: string = 'voteAuthorizeWithSeedArgs', +) => { + return BufferLayout.struct( + [ + BufferLayout.u32('voteAuthorizationType'), + publicKey('currentAuthorityDerivedKeyOwnerPubkey'), + rustString('currentAuthorityDerivedKeySeed'), + publicKey('newAuthorized'), + ], + property, + ); +}; + export function getAlloc(type: any, fields: any): number { const getItemAlloc = (item: any): number => { if (item.span >= 0) { diff --git a/web3.js/src/programs/vote.ts b/web3.js/src/programs/vote.ts index db1a111da..e7d75350e 100644 --- a/web3.js/src/programs/vote.ts +++ b/web3.js/src/programs/vote.ts @@ -65,6 +65,18 @@ export type AuthorizeVoteParams = { voteAuthorizationType: VoteAuthorizationType; }; +/** + * AuthorizeWithSeed instruction params + */ +export type AuthorizeVoteWithSeedParams = { + currentAuthorityDerivedKeyBasePubkey: PublicKey; + currentAuthorityDerivedKeyOwnerPubkey: PublicKey; + currentAuthorityDerivedKeySeed: string; + newAuthorizedPubkey: PublicKey; + voteAuthorizationType: VoteAuthorizationType; + votePubkey: PublicKey; +}; + /** * Withdraw from vote account transaction params */ @@ -160,6 +172,41 @@ export class VoteInstruction { }; } + /** + * Decode an authorize instruction and retrieve the instruction params. + */ + static decodeAuthorizeWithSeed( + instruction: TransactionInstruction, + ): AuthorizeVoteWithSeedParams { + this.checkProgramId(instruction.programId); + this.checkKeyLength(instruction.keys, 3); + + const { + voteAuthorizeWithSeedArgs: { + currentAuthorityDerivedKeyOwnerPubkey, + currentAuthorityDerivedKeySeed, + newAuthorized, + voteAuthorizationType, + }, + } = decodeData( + VOTE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed, + instruction.data, + ); + + return { + currentAuthorityDerivedKeyBasePubkey: instruction.keys[2].pubkey, + currentAuthorityDerivedKeyOwnerPubkey: new PublicKey( + currentAuthorityDerivedKeyOwnerPubkey, + ), + currentAuthorityDerivedKeySeed: currentAuthorityDerivedKeySeed, + newAuthorizedPubkey: new PublicKey(newAuthorized), + voteAuthorizationType: { + index: voteAuthorizationType, + }, + votePubkey: instruction.keys[0].pubkey, + }; + } + /** * Decode a withdraw instruction and retrieve the instruction params. */ @@ -211,13 +258,23 @@ export type VoteInstructionType = // It would be preferable for this type to be `keyof VoteInstructionInputData` // but Typedoc does not transpile `keyof` expressions. // See https://github.com/TypeStrong/typedoc/issues/1894 - 'Authorize' | 'InitializeAccount' | 'Withdraw'; + 'Authorize' | 'AuthorizeWithSeed' | 'InitializeAccount' | 'Withdraw'; +/** @internal */ +export type VoteAuthorizeWithSeedArgs = Readonly<{ + currentAuthorityDerivedKeyOwnerPubkey: Uint8Array; + currentAuthorityDerivedKeySeed: string; + newAuthorized: Uint8Array; + voteAuthorizationType: number; +}>; type VoteInstructionInputData = { Authorize: IInstructionInputData & { newAuthorized: Uint8Array; voteAuthorizationType: number; }; + AuthorizeWithSeed: IInstructionInputData & { + voteAuthorizeWithSeedArgs: VoteAuthorizeWithSeedArgs; + }; InitializeAccount: IInstructionInputData & { voteInit: Readonly<{ authorizedVoter: Uint8Array; @@ -258,6 +315,13 @@ const VOTE_INSTRUCTION_LAYOUTS = Object.freeze<{ BufferLayout.ns64('lamports'), ]), }, + AuthorizeWithSeed: { + index: 10, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.voteAuthorizeWithSeedArgs(), + ]), + }, }); /** @@ -390,6 +454,49 @@ export class VoteProgram { }); } + /** + * Generate a transaction that authorizes a new Voter or Withdrawer on the Vote account + * where the current Voter or Withdrawer authority is a derived key. + */ + static authorizeWithSeed(params: AuthorizeVoteWithSeedParams): Transaction { + const { + currentAuthorityDerivedKeyBasePubkey, + currentAuthorityDerivedKeyOwnerPubkey, + currentAuthorityDerivedKeySeed, + newAuthorizedPubkey, + voteAuthorizationType, + votePubkey, + } = params; + + const type = VOTE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed; + const data = encodeData(type, { + voteAuthorizeWithSeedArgs: { + currentAuthorityDerivedKeyOwnerPubkey: toBuffer( + currentAuthorityDerivedKeyOwnerPubkey.toBuffer(), + ), + currentAuthorityDerivedKeySeed: currentAuthorityDerivedKeySeed, + newAuthorized: toBuffer(newAuthorizedPubkey.toBuffer()), + voteAuthorizationType: voteAuthorizationType.index, + }, + }); + + const keys = [ + {pubkey: votePubkey, isSigner: false, isWritable: true}, + {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, + { + pubkey: currentAuthorityDerivedKeyBasePubkey, + isSigner: true, + isWritable: false, + }, + ]; + + return new Transaction().add({ + keys, + programId: this.programId, + data, + }); + } + /** * Generate a transaction to withdraw from a Vote account. */ diff --git a/web3.js/test/program-tests/vote.test.ts b/web3.js/test/program-tests/vote.test.ts index 6cd349a0c..bb8cc57ed 100644 --- a/web3.js/test/program-tests/vote.test.ts +++ b/web3.js/test/program-tests/vote.test.ts @@ -11,6 +11,7 @@ import { sendAndConfirmTransaction, SystemInstruction, Connection, + PublicKey, } from '../../src'; import {helpers} from '../mocks/rpc-http'; import {url} from '../url'; @@ -96,6 +97,29 @@ describe('VoteProgram', () => { ); }); + it('authorize with seed', () => { + const votePubkey = Keypair.generate().publicKey; + const currentAuthorityDerivedKeyBasePubkey = Keypair.generate().publicKey; + const currentAuthorityDerivedKeyOwnerPubkey = Keypair.generate().publicKey; + const currentAuthorityDerivedKeySeed = 'sunflower'; + const newAuthorizedPubkey = Keypair.generate().publicKey; + const voteAuthorizationType = VoteAuthorizationLayout.Voter; + const params = { + currentAuthorityDerivedKeyBasePubkey, + currentAuthorityDerivedKeyOwnerPubkey, + currentAuthorityDerivedKeySeed, + newAuthorizedPubkey, + voteAuthorizationType, + votePubkey, + }; + const transaction = VoteProgram.authorizeWithSeed(params); + expect(transaction.instructions).to.have.length(1); + const [authorizeWithSeedInstruction] = transaction.instructions; + expect(params).to.eql( + VoteInstruction.decodeAuthorizeWithSeed(authorizeWithSeedInstruction), + ); + }); + it('withdraw', () => { const votePubkey = Keypair.generate().publicKey; const authorizedWithdrawerPubkey = Keypair.generate().publicKey; @@ -113,6 +137,107 @@ describe('VoteProgram', () => { }); if (process.env.TEST_LIVE) { + it('change authority from derived key', async () => { + const connection = new Connection(url, 'confirmed'); + + const newVoteAccount = Keypair.generate(); + const nodeAccount = Keypair.generate(); + const derivedKeyOwnerProgram = Keypair.generate(); + const derivedKeySeed = 'sunflower'; + const newAuthorizedWithdrawer = Keypair.generate(); + + const derivedKeyBaseKeypair = Keypair.generate(); + const [ + _1, // eslint-disable-line @typescript-eslint/no-unused-vars + _2, // eslint-disable-line @typescript-eslint/no-unused-vars + minimumAmount, + derivedKey, + ] = await Promise.all([ + (async () => { + await helpers.airdrop({ + connection, + address: derivedKeyBaseKeypair.publicKey, + amount: 12 * LAMPORTS_PER_SOL, + }); + expect( + await connection.getBalance(derivedKeyBaseKeypair.publicKey), + ).to.eq(12 * LAMPORTS_PER_SOL); + })(), + (async () => { + await helpers.airdrop({ + connection, + address: newAuthorizedWithdrawer.publicKey, + amount: 0.1 * LAMPORTS_PER_SOL, + }); + expect( + await connection.getBalance(newAuthorizedWithdrawer.publicKey), + ).to.eq(0.1 * LAMPORTS_PER_SOL); + })(), + connection.getMinimumBalanceForRentExemption(VoteProgram.space), + PublicKey.createWithSeed( + derivedKeyBaseKeypair.publicKey, + derivedKeySeed, + derivedKeyOwnerProgram.publicKey, + ), + ]); + + // Create initialized Vote account + const createAndInitialize = VoteProgram.createAccount({ + fromPubkey: derivedKeyBaseKeypair.publicKey, + votePubkey: newVoteAccount.publicKey, + voteInit: new VoteInit( + nodeAccount.publicKey, + derivedKey, + derivedKey, + 5, + ), + lamports: minimumAmount + 10 * LAMPORTS_PER_SOL, + }); + await sendAndConfirmTransaction( + connection, + createAndInitialize, + [derivedKeyBaseKeypair, newVoteAccount, nodeAccount], + {preflightCommitment: 'confirmed'}, + ); + expect(await connection.getBalance(newVoteAccount.publicKey)).to.eq( + minimumAmount + 10 * LAMPORTS_PER_SOL, + ); + + // Authorize a new Withdrawer. + const authorize = VoteProgram.authorizeWithSeed({ + currentAuthorityDerivedKeyBasePubkey: derivedKeyBaseKeypair.publicKey, + currentAuthorityDerivedKeyOwnerPubkey: derivedKeyOwnerProgram.publicKey, + currentAuthorityDerivedKeySeed: derivedKeySeed, + newAuthorizedPubkey: newAuthorizedWithdrawer.publicKey, + voteAuthorizationType: VoteAuthorizationLayout.Withdrawer, + votePubkey: newVoteAccount.publicKey, + }); + await sendAndConfirmTransaction( + connection, + authorize, + [derivedKeyBaseKeypair], + {preflightCommitment: 'confirmed'}, + ); + + // Test newAuthorizedWithdrawer may withdraw. + const recipient = Keypair.generate(); + const withdraw = VoteProgram.withdraw({ + votePubkey: newVoteAccount.publicKey, + authorizedWithdrawerPubkey: newAuthorizedWithdrawer.publicKey, + lamports: LAMPORTS_PER_SOL, + toPubkey: recipient.publicKey, + }); + await sendAndConfirmTransaction( + connection, + withdraw, + [newAuthorizedWithdrawer], + {preflightCommitment: 'confirmed'}, + ); + expect(await connection.getBalance(recipient.publicKey)).to.eq( + LAMPORTS_PER_SOL, + ); + }); + it('live vote actions', async () => { const connection = new Connection(url, 'confirmed');