feat(web3.js): support withdraw from Vote account (#22932)

This commit is contained in:
mooori 2022-02-04 21:37:28 +01:00 committed by GitHub
parent 96c88d1a5e
commit 4d877567dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 472 additions and 0 deletions

View File

@ -17,6 +17,7 @@ export * from './secp256k1-program';
export * from './transaction';
export * from './validator-info';
export * from './vote-account';
export * from './vote-program';
export * from './sysvar';
export * from './errors';
export * from './util/borsh-schema';

View File

@ -79,6 +79,21 @@ export const lockup = (property: string = 'lockup') => {
);
};
/**
* Layout for a VoteInit object
*/
export const voteInit = (property: string = 'voteInit') => {
return BufferLayout.struct(
[
publicKey('nodePubkey'),
publicKey('authorizedVoter'),
publicKey('authorizedWithdrawer'),
BufferLayout.u8('commission'),
],
property,
);
};
export function getAlloc(type: any, fields: any): number {
let alloc = 0;
type.layout.fields.forEach((item: any) => {

290
web3.js/src/vote-program.ts Normal file
View File

@ -0,0 +1,290 @@
import * as BufferLayout from '@solana/buffer-layout';
import {encodeData, decodeData, InstructionType} from './instruction';
import * as Layout from './layout';
import {PublicKey} from './publickey';
import {SystemProgram} from './system-program';
import {SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY} from './sysvar';
import {Transaction, TransactionInstruction} from './transaction';
import {toBuffer} from './util/to-buffer';
/**
* Vote account info
*/
export class VoteInit {
nodePubkey: PublicKey;
authorizedVoter: PublicKey;
authorizedWithdrawer: PublicKey;
commission: number; /** [0, 100] */
constructor(
nodePubkey: PublicKey,
authorizedVoter: PublicKey,
authorizedWithdrawer: PublicKey,
commission: number,
) {
this.nodePubkey = nodePubkey;
this.authorizedVoter = authorizedVoter;
this.authorizedWithdrawer = authorizedWithdrawer;
this.commission = commission;
}
}
/**
* Create vote account transaction params
*/
export type CreateVoteAccountParams = {
fromPubkey: PublicKey;
votePubkey: PublicKey;
voteInit: VoteInit;
lamports: number;
};
/**
* InitializeAccount instruction params
*/
export type InitializeAccountParams = {
votePubkey: PublicKey;
nodePubkey: PublicKey;
voteInit: VoteInit;
};
/**
* Withdraw from vote account transaction params
*/
export type WithdrawFromVoteAccountParams = {
votePubkey: PublicKey;
authorizedWithdrawerPubkey: PublicKey;
lamports: number;
toPubkey: PublicKey;
};
/**
* Vote Instruction class
*/
export class VoteInstruction {
/**
* @internal
*/
constructor() {}
/**
* Decode a vote instruction and retrieve the instruction type.
*/
static decodeInstructionType(
instruction: TransactionInstruction,
): VoteInstructionType {
this.checkProgramId(instruction.programId);
const instructionTypeLayout = BufferLayout.u32('instruction');
const typeIndex = instructionTypeLayout.decode(instruction.data);
let type: VoteInstructionType | undefined;
for (const [ixType, layout] of Object.entries(VOTE_INSTRUCTION_LAYOUTS)) {
if (layout.index == typeIndex) {
type = ixType as VoteInstructionType;
break;
}
}
if (!type) {
throw new Error('Instruction type incorrect; not a VoteInstruction');
}
return type;
}
/**
* Decode an initialize vote instruction and retrieve the instruction params.
*/
static decodeInitializeAccount(
instruction: TransactionInstruction,
): InitializeAccountParams {
this.checkProgramId(instruction.programId);
this.checkKeyLength(instruction.keys, 4);
const {voteInit} = decodeData(
VOTE_INSTRUCTION_LAYOUTS.InitializeAccount,
instruction.data,
);
return {
votePubkey: instruction.keys[0].pubkey,
nodePubkey: instruction.keys[3].pubkey,
voteInit: new VoteInit(
new PublicKey(voteInit.nodePubkey),
new PublicKey(voteInit.authorizedVoter),
new PublicKey(voteInit.authorizedWithdrawer),
voteInit.commission,
),
};
}
/**
* Decode a withdraw instruction and retrieve the instruction params.
*/
static decodeWithdraw(
instruction: TransactionInstruction,
): WithdrawFromVoteAccountParams {
this.checkProgramId(instruction.programId);
this.checkKeyLength(instruction.keys, 3);
const {lamports} = decodeData(
VOTE_INSTRUCTION_LAYOUTS.Withdraw,
instruction.data,
);
return {
votePubkey: instruction.keys[0].pubkey,
authorizedWithdrawerPubkey: instruction.keys[2].pubkey,
lamports,
toPubkey: instruction.keys[1].pubkey,
};
}
/**
* @internal
*/
static checkProgramId(programId: PublicKey) {
if (!programId.equals(VoteProgram.programId)) {
throw new Error('invalid instruction; programId is not VoteProgram');
}
}
/**
* @internal
*/
static checkKeyLength(keys: Array<any>, expectedLength: number) {
if (keys.length < expectedLength) {
throw new Error(
`invalid instruction; found ${keys.length} keys, expected at least ${expectedLength}`,
);
}
}
}
/**
* An enumeration of valid VoteInstructionType's
*/
export type VoteInstructionType = 'InitializeAccount' | 'Withdraw';
const VOTE_INSTRUCTION_LAYOUTS: {
[type in VoteInstructionType]: InstructionType;
} = Object.freeze({
InitializeAccount: {
index: 0,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
Layout.voteInit(),
]),
},
Withdraw: {
index: 3,
layout: BufferLayout.struct([
BufferLayout.u32('instruction'),
BufferLayout.ns64('lamports'),
]),
},
});
/**
* Factory class for transactions to interact with the Vote program
*/
export class VoteProgram {
/**
* @internal
*/
constructor() {}
/**
* Public key that identifies the Vote program
*/
static programId: PublicKey = new PublicKey(
'Vote111111111111111111111111111111111111111',
);
/**
* Max space of a Vote account
*
* This is generated from the solana-vote-program VoteState struct as
* `VoteState::size_of()`:
* https://docs.rs/solana-vote-program/1.9.5/solana_vote_program/vote_state/struct.VoteState.html#method.size_of
*/
static space: number = 3731;
/**
* Generate an Initialize instruction.
*/
static initializeAccount(
params: InitializeAccountParams,
): TransactionInstruction {
const {votePubkey, nodePubkey, voteInit} = params;
const type = VOTE_INSTRUCTION_LAYOUTS.InitializeAccount;
const data = encodeData(type, {
voteInit: {
nodePubkey: toBuffer(voteInit.nodePubkey.toBuffer()),
authorizedVoter: toBuffer(voteInit.authorizedVoter.toBuffer()),
authorizedWithdrawer: toBuffer(
voteInit.authorizedWithdrawer.toBuffer(),
),
commission: voteInit.commission,
},
});
const instructionData = {
keys: [
{pubkey: votePubkey, isSigner: false, isWritable: true},
{pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
{pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false},
{pubkey: nodePubkey, isSigner: true, isWritable: false},
],
programId: this.programId,
data,
};
return new TransactionInstruction(instructionData);
}
/**
* Generate a transaction that creates a new Vote account.
*/
static createAccount(params: CreateVoteAccountParams): Transaction {
const transaction = new Transaction();
transaction.add(
SystemProgram.createAccount({
fromPubkey: params.fromPubkey,
newAccountPubkey: params.votePubkey,
lamports: params.lamports,
space: this.space,
programId: this.programId,
}),
);
return transaction.add(
this.initializeAccount({
votePubkey: params.votePubkey,
nodePubkey: params.voteInit.nodePubkey,
voteInit: params.voteInit,
}),
);
}
/**
* Generate a transaction to withdraw from a Vote account.
*/
static withdraw(params: WithdrawFromVoteAccountParams): Transaction {
const {votePubkey, authorizedWithdrawerPubkey, lamports, toPubkey} = params;
const type = VOTE_INSTRUCTION_LAYOUTS.Withdraw;
const data = encodeData(type, {lamports});
const keys = [
{pubkey: votePubkey, isSigner: false, isWritable: true},
{pubkey: toPubkey, isSigner: false, isWritable: true},
{pubkey: authorizedWithdrawerPubkey, isSigner: true, isWritable: false},
];
return new Transaction().add({
keys,
programId: this.programId,
data,
});
}
}

View File

@ -0,0 +1,166 @@
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {
Keypair,
LAMPORTS_PER_SOL,
VoteInit,
VoteInstruction,
VoteProgram,
sendAndConfirmTransaction,
SystemInstruction,
Connection,
} from '../src';
import {helpers} from './mocks/rpc-http';
import {url} from './url';
use(chaiAsPromised);
describe('VoteProgram', () => {
it('createAccount', () => {
const fromPubkey = Keypair.generate().publicKey;
const newAccountPubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const nodePubkey = Keypair.generate().publicKey;
const commission = 5;
const voteInit = new VoteInit(
nodePubkey,
authorizedPubkey,
authorizedPubkey,
commission,
);
const lamports = 123;
const transaction = VoteProgram.createAccount({
fromPubkey,
votePubkey: newAccountPubkey,
voteInit,
lamports,
});
expect(transaction.instructions).to.have.length(2);
const [systemInstruction, voteInstruction] = transaction.instructions;
const systemParams = {
fromPubkey,
newAccountPubkey,
lamports,
space: VoteProgram.space,
programId: VoteProgram.programId,
};
expect(systemParams).to.eql(
SystemInstruction.decodeCreateAccount(systemInstruction),
);
const initParams = {votePubkey: newAccountPubkey, nodePubkey, voteInit};
expect(initParams).to.eql(
VoteInstruction.decodeInitializeAccount(voteInstruction),
);
});
it('initialize', () => {
const newAccountPubkey = Keypair.generate().publicKey;
const authorizedPubkey = Keypair.generate().publicKey;
const nodePubkey = Keypair.generate().publicKey;
const voteInit = new VoteInit(
nodePubkey,
authorizedPubkey,
authorizedPubkey,
5,
);
const initParams = {
votePubkey: newAccountPubkey,
nodePubkey,
voteInit,
};
const initInstruction = VoteProgram.initializeAccount(initParams);
expect(initParams).to.eql(
VoteInstruction.decodeInitializeAccount(initInstruction),
);
});
it('withdraw', () => {
const votePubkey = Keypair.generate().publicKey;
const authorizedWithdrawerPubkey = Keypair.generate().publicKey;
const toPubkey = Keypair.generate().publicKey;
const params = {
votePubkey,
authorizedWithdrawerPubkey,
lamports: 123,
toPubkey,
};
const transaction = VoteProgram.withdraw(params);
expect(transaction.instructions).to.have.length(1);
const [withdrawInstruction] = transaction.instructions;
expect(params).to.eql(VoteInstruction.decodeWithdraw(withdrawInstruction));
});
if (process.env.TEST_LIVE) {
it('live vote actions', async () => {
const connection = new Connection(url, 'confirmed');
const newVoteAccount = Keypair.generate();
const nodeAccount = Keypair.generate();
const payer = Keypair.generate();
await helpers.airdrop({
connection,
address: payer.publicKey,
amount: 12 * LAMPORTS_PER_SOL,
});
expect(await connection.getBalance(payer.publicKey)).to.eq(
12 * LAMPORTS_PER_SOL,
);
const authorized = Keypair.generate();
await helpers.airdrop({
connection,
address: authorized.publicKey,
amount: 12 * LAMPORTS_PER_SOL,
});
expect(await connection.getBalance(authorized.publicKey)).to.eq(
12 * LAMPORTS_PER_SOL,
);
const minimumAmount = await connection.getMinimumBalanceForRentExemption(
VoteProgram.space,
);
// Create initialized Vote account
let createAndInitialize = VoteProgram.createAccount({
fromPubkey: payer.publicKey,
votePubkey: newVoteAccount.publicKey,
voteInit: new VoteInit(
nodeAccount.publicKey,
authorized.publicKey,
authorized.publicKey,
5,
),
lamports: minimumAmount + 2 * 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,
);
// Withdraw from Vote account
const 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,
);
}).timeout(10 * 1000);
}
});