feat(vote-program): support VoteInstruction::Authorize (#22978)
This commit is contained in:
@ -49,6 +49,17 @@ export type InitializeAccountParams = {
|
|||||||
voteInit: VoteInit;
|
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
|
* 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.
|
* Decode a withdraw instruction and retrieve the instruction params.
|
||||||
*/
|
*/
|
||||||
@ -166,7 +201,10 @@ export class VoteInstruction {
|
|||||||
/**
|
/**
|
||||||
* An enumeration of valid VoteInstructionType's
|
* An enumeration of valid VoteInstructionType's
|
||||||
*/
|
*/
|
||||||
export type VoteInstructionType = 'InitializeAccount' | 'Withdraw';
|
export type VoteInstructionType =
|
||||||
|
| 'Authorize'
|
||||||
|
| 'InitializeAccount'
|
||||||
|
| 'Withdraw';
|
||||||
|
|
||||||
const VOTE_INSTRUCTION_LAYOUTS: {
|
const VOTE_INSTRUCTION_LAYOUTS: {
|
||||||
[type in VoteInstructionType]: InstructionType;
|
[type in VoteInstructionType]: InstructionType;
|
||||||
@ -178,6 +216,14 @@ const VOTE_INSTRUCTION_LAYOUTS: {
|
|||||||
Layout.voteInit(),
|
Layout.voteInit(),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
Authorize: {
|
||||||
|
index: 1,
|
||||||
|
layout: BufferLayout.struct([
|
||||||
|
BufferLayout.u32('instruction'),
|
||||||
|
Layout.publicKey('newAuthorized'),
|
||||||
|
BufferLayout.u32('voteAuthorizationType'),
|
||||||
|
]),
|
||||||
|
},
|
||||||
Withdraw: {
|
Withdraw: {
|
||||||
index: 3,
|
index: 3,
|
||||||
layout: BufferLayout.struct([
|
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
|
* 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.
|
* Generate a transaction to withdraw from a Vote account.
|
||||||
*/
|
*/
|
||||||
|
@ -4,6 +4,7 @@ import chaiAsPromised from 'chai-as-promised';
|
|||||||
import {
|
import {
|
||||||
Keypair,
|
Keypair,
|
||||||
LAMPORTS_PER_SOL,
|
LAMPORTS_PER_SOL,
|
||||||
|
VoteAuthorizationLayout,
|
||||||
VoteInit,
|
VoteInit,
|
||||||
VoteInstruction,
|
VoteInstruction,
|
||||||
VoteProgram,
|
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', () => {
|
it('withdraw', () => {
|
||||||
const votePubkey = Keypair.generate().publicKey;
|
const votePubkey = Keypair.generate().publicKey;
|
||||||
const authorizedWithdrawerPubkey = Keypair.generate().publicKey;
|
const authorizedWithdrawerPubkey = Keypair.generate().publicKey;
|
||||||
@ -133,9 +153,8 @@ describe('VoteProgram', () => {
|
|||||||
authorized.publicKey,
|
authorized.publicKey,
|
||||||
5,
|
5,
|
||||||
),
|
),
|
||||||
lamports: minimumAmount + 2 * LAMPORTS_PER_SOL,
|
lamports: minimumAmount + 10 * LAMPORTS_PER_SOL,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendAndConfirmTransaction(
|
await sendAndConfirmTransaction(
|
||||||
connection,
|
connection,
|
||||||
createAndInitialize,
|
createAndInitialize,
|
||||||
@ -143,24 +162,104 @@ describe('VoteProgram', () => {
|
|||||||
{preflightCommitment: 'confirmed'},
|
{preflightCommitment: 'confirmed'},
|
||||||
);
|
);
|
||||||
expect(await connection.getBalance(newVoteAccount.publicKey)).to.eq(
|
expect(await connection.getBalance(newVoteAccount.publicKey)).to.eq(
|
||||||
minimumAmount + 2 * LAMPORTS_PER_SOL,
|
minimumAmount + 10 * LAMPORTS_PER_SOL,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Withdraw from Vote account
|
// Withdraw from Vote account
|
||||||
const recipient = Keypair.generate();
|
let recipient = Keypair.generate();
|
||||||
let withdraw = VoteProgram.withdraw({
|
let withdraw = VoteProgram.withdraw({
|
||||||
votePubkey: newVoteAccount.publicKey,
|
votePubkey: newVoteAccount.publicKey,
|
||||||
authorizedWithdrawerPubkey: authorized.publicKey,
|
authorizedWithdrawerPubkey: authorized.publicKey,
|
||||||
lamports: LAMPORTS_PER_SOL,
|
lamports: LAMPORTS_PER_SOL,
|
||||||
toPubkey: recipient.publicKey,
|
toPubkey: recipient.publicKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendAndConfirmTransaction(connection, withdraw, [authorized], {
|
await sendAndConfirmTransaction(connection, withdraw, [authorized], {
|
||||||
preflightCommitment: 'confirmed',
|
preflightCommitment: 'confirmed',
|
||||||
});
|
});
|
||||||
expect(await connection.getBalance(recipient.publicKey)).to.eq(
|
expect(await connection.getBalance(recipient.publicKey)).to.eq(
|
||||||
LAMPORTS_PER_SOL,
|
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);
|
}).timeout(10 * 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user