Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added authorizeWithSeed to the vote program in web3.js #27627

Merged
merged 2 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 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 All @@ -152,6 +171,9 @@ export function getAlloc(type: any, fields: any): number {
if (Array.isArray(field)) {
return field.length * getItemAlloc(item.elementLayout);
}
} else if ('fields' in item) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go review this in #27624.

// This is a `Structure` whose size needs to be recursively measured.
return getAlloc({layout: item}, fields[item.property]);
}
// Couldn't determine allocated size of layout
return 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