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
This commit is contained in:
Steven Luscher 2022-09-06 22:37:43 -07:00 committed by GitHub
parent a94ada8b3e
commit 466e9948bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 252 additions and 1 deletions

View File

@ -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<VoteAuthorizeWithSeedArgs>(
[
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) {

View File

@ -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<VoteInstructionInputData['AuthorizeWithSeed']>([
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.
*/

View File

@ -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');