Skip to content

Commit

Permalink
feat: added authorizeWithSeed to the vote program in web3.js
Browse files Browse the repository at this point in the history
  • Loading branch information
steveluscher committed Sep 6, 2022
1 parent 1d37805 commit 848f499
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 2 deletions.
19 changes: 19 additions & 0 deletions web3.js/src/layout.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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) {
Expand Down
111 changes: 109 additions & 2 deletions web3.js/src/programs/vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
]),
},
});

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
125 changes: 125 additions & 0 deletions web3.js/test/program-tests/vote.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
sendAndConfirmTransaction,
SystemInstruction,
Connection,
PublicKey,
} from '../../src';
import {helpers} from '../mocks/rpc-http';
import {url} from '../url';
Expand Down Expand Up @@ -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;
Expand All @@ -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');

Expand Down

0 comments on commit 848f499

Please sign in to comment.