feat(web3.js): support withdraw from Vote account (#22932)
This commit is contained in:
parent
96c88d1a5e
commit
4d877567dd
|
@ -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';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue