Skip to content

Commit

Permalink
feat(vote-program): support VoteInstruction::Authorize (solana-labs#2…
Browse files Browse the repository at this point in the history
  • Loading branch information
mooori authored Feb 9, 2022
1 parent 86d465c commit ae175a0
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 6 deletions.
98 changes: 97 additions & 1 deletion web3.js/src/vote-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand All @@ -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([
Expand All @@ -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
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
109 changes: 104 additions & 5 deletions web3.js/test/vote-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import chaiAsPromised from 'chai-as-promised';
import {
Keypair,
LAMPORTS_PER_SOL,
VoteAuthorizationLayout,
VoteInit,
VoteInstruction,
VoteProgram,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -133,34 +153,113 @@ describe('VoteProgram', () => {
authorized.publicKey,
5,
),
lamports: minimumAmount + 2 * LAMPORTS_PER_SOL,
lamports: minimumAmount + 10 * LAMPORTS_PER_SOL,
});

await sendAndConfirmTransaction(
connection,
createAndInitialize,
[payer, newVoteAccount, nodeAccount],
{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);
}
});

0 comments on commit ae175a0

Please sign in to comment.