diff --git a/web3.js/src/vote-program.ts b/web3.js/src/vote-program.ts index 2557504cde..272a6b0da1 100644 --- a/web3.js/src/vote-program.ts +++ b/web3.js/src/vote-program.ts @@ -49,6 +49,17 @@ export type InitializeAccountParams = { voteInit: VoteInit; }; +/** + * Authorize instruction params + */ +export type AuthorizeVoteParams = { + votePubkey: PublicKey; + /** Current vote or withdraw authority, depending on `voteAuthorizationType` */ + authorizedPubkey: PublicKey; + newAuthorizedPubkey: PublicKey; + voteAuthorizationType: VoteAuthorizationType; +}; + /** * Withdraw from vote account transaction params */ @@ -120,6 +131,30 @@ export class VoteInstruction { }; } + /** + * Decode an authorize instruction and retrieve the instruction params. + */ + static decodeAuthorize( + instruction: TransactionInstruction, + ): AuthorizeVoteParams { + this.checkProgramId(instruction.programId); + this.checkKeyLength(instruction.keys, 3); + + const {newAuthorized, voteAuthorizationType} = decodeData( + VOTE_INSTRUCTION_LAYOUTS.Authorize, + instruction.data, + ); + + return { + votePubkey: instruction.keys[0].pubkey, + authorizedPubkey: instruction.keys[2].pubkey, + newAuthorizedPubkey: new PublicKey(newAuthorized), + voteAuthorizationType: { + index: voteAuthorizationType, + }, + }; + } + /** * Decode a withdraw instruction and retrieve the instruction params. */ @@ -166,7 +201,10 @@ export class VoteInstruction { /** * An enumeration of valid VoteInstructionType's */ -export type VoteInstructionType = 'InitializeAccount' | 'Withdraw'; +export type VoteInstructionType = + | 'Authorize' + | 'InitializeAccount' + | 'Withdraw'; const VOTE_INSTRUCTION_LAYOUTS: { [type in VoteInstructionType]: InstructionType; @@ -178,6 +216,14 @@ const VOTE_INSTRUCTION_LAYOUTS: { Layout.voteInit(), ]), }, + Authorize: { + index: 1, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.publicKey('newAuthorized'), + BufferLayout.u32('voteAuthorizationType'), + ]), + }, Withdraw: { index: 3, layout: BufferLayout.struct([ @@ -187,6 +233,26 @@ const VOTE_INSTRUCTION_LAYOUTS: { }, }); +/** + * VoteAuthorize type + */ +export type VoteAuthorizationType = { + /** The VoteAuthorize index (from solana-vote-program) */ + index: number; +}; + +/** + * An enumeration of valid VoteAuthorization layouts. + */ +export const VoteAuthorizationLayout = Object.freeze({ + Voter: { + index: 0, + }, + Withdrawer: { + index: 1, + }, +}); + /** * Factory class for transactions to interact with the Vote program */ @@ -267,6 +333,36 @@ export class VoteProgram { ); } + /** + * Generate a transaction that authorizes a new Voter or Withdrawer on the Vote account. + */ + static authorize(params: AuthorizeVoteParams): Transaction { + const { + votePubkey, + authorizedPubkey, + newAuthorizedPubkey, + voteAuthorizationType, + } = params; + + const type = VOTE_INSTRUCTION_LAYOUTS.Authorize; + const data = encodeData(type, { + newAuthorized: toBuffer(newAuthorizedPubkey.toBuffer()), + voteAuthorizationType: voteAuthorizationType.index, + }); + + const keys = [ + {pubkey: votePubkey, isSigner: false, isWritable: true}, + {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, + {pubkey: authorizedPubkey, 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/vote-program.test.ts b/web3.js/test/vote-program.test.ts index e8bb371c45..aab52b07f0 100644 --- a/web3.js/test/vote-program.test.ts +++ b/web3.js/test/vote-program.test.ts @@ -4,6 +4,7 @@ import chaiAsPromised from 'chai-as-promised'; import { Keypair, LAMPORTS_PER_SOL, + VoteAuthorizationLayout, VoteInit, VoteInstruction, VoteProgram, @@ -76,6 +77,25 @@ describe('VoteProgram', () => { ); }); + it('authorize', () => { + const votePubkey = Keypair.generate().publicKey; + const authorizedPubkey = Keypair.generate().publicKey; + const newAuthorizedPubkey = Keypair.generate().publicKey; + const voteAuthorizationType = VoteAuthorizationLayout.Voter; + const params = { + votePubkey, + authorizedPubkey, + newAuthorizedPubkey, + voteAuthorizationType, + }; + const transaction = VoteProgram.authorize(params); + expect(transaction.instructions).to.have.length(1); + const [authorizeInstruction] = transaction.instructions; + expect(params).to.eql( + VoteInstruction.decodeAuthorize(authorizeInstruction), + ); + }); + it('withdraw', () => { const votePubkey = Keypair.generate().publicKey; const authorizedWithdrawerPubkey = Keypair.generate().publicKey; @@ -133,9 +153,8 @@ describe('VoteProgram', () => { authorized.publicKey, 5, ), - lamports: minimumAmount + 2 * LAMPORTS_PER_SOL, + lamports: minimumAmount + 10 * LAMPORTS_PER_SOL, }); - await sendAndConfirmTransaction( connection, createAndInitialize, @@ -143,24 +162,104 @@ describe('VoteProgram', () => { {preflightCommitment: 'confirmed'}, ); expect(await connection.getBalance(newVoteAccount.publicKey)).to.eq( - minimumAmount + 2 * LAMPORTS_PER_SOL, + minimumAmount + 10 * LAMPORTS_PER_SOL, ); // Withdraw from Vote account - const recipient = Keypair.generate(); + let recipient = Keypair.generate(); let withdraw = VoteProgram.withdraw({ votePubkey: newVoteAccount.publicKey, authorizedWithdrawerPubkey: authorized.publicKey, lamports: LAMPORTS_PER_SOL, toPubkey: recipient.publicKey, }); - await sendAndConfirmTransaction(connection, withdraw, [authorized], { preflightCommitment: 'confirmed', }); expect(await connection.getBalance(recipient.publicKey)).to.eq( LAMPORTS_PER_SOL, ); + + const newAuthorizedWithdrawer = Keypair.generate(); + await helpers.airdrop({ + connection, + address: newAuthorizedWithdrawer.publicKey, + amount: LAMPORTS_PER_SOL, + }); + expect( + await connection.getBalance(newAuthorizedWithdrawer.publicKey), + ).to.eq(LAMPORTS_PER_SOL); + + // Authorize a new Withdrawer. + let authorize = VoteProgram.authorize({ + votePubkey: newVoteAccount.publicKey, + authorizedPubkey: authorized.publicKey, + newAuthorizedPubkey: newAuthorizedWithdrawer.publicKey, + voteAuthorizationType: VoteAuthorizationLayout.Withdrawer, + }); + await sendAndConfirmTransaction(connection, authorize, [authorized], { + preflightCommitment: 'confirmed', + }); + + // Test old authorized cannot withdraw anymore. + withdraw = VoteProgram.withdraw({ + votePubkey: newVoteAccount.publicKey, + authorizedWithdrawerPubkey: authorized.publicKey, + lamports: minimumAmount, + toPubkey: recipient.publicKey, + }); + await expect( + sendAndConfirmTransaction(connection, withdraw, [authorized], { + preflightCommitment: 'confirmed', + }), + ).to.be.rejected; + + // Test newAuthorizedWithdrawer may withdraw. + recipient = Keypair.generate(); + 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, + ); + + const newAuthorizedVoter = Keypair.generate(); + await helpers.airdrop({ + connection, + address: newAuthorizedVoter.publicKey, + amount: LAMPORTS_PER_SOL, + }); + expect(await connection.getBalance(newAuthorizedVoter.publicKey)).to.eq( + LAMPORTS_PER_SOL, + ); + + // The authorized Withdrawer may sign to authorize a new Voter, see + // https://github.com/solana-labs/solana/issues/22521 + authorize = VoteProgram.authorize({ + votePubkey: newVoteAccount.publicKey, + authorizedPubkey: newAuthorizedWithdrawer.publicKey, + newAuthorizedPubkey: newAuthorizedVoter.publicKey, + voteAuthorizationType: VoteAuthorizationLayout.Voter, + }); + await sendAndConfirmTransaction( + connection, + authorize, + [newAuthorizedWithdrawer], + { + preflightCommitment: 'confirmed', + }, + ); }).timeout(10 * 1000); } });