import * as BufferLayout from '@solana/buffer-layout'; import { encodeData, decodeData, InstructionType, IInstructionInputData, } 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; }; /** * Authorize instruction params */ export type AuthorizeVoteParams = { votePubkey: PublicKey; /** Current vote or withdraw authority, depending on `voteAuthorizationType` */ authorizedPubkey: PublicKey; newAuthorizedPubkey: PublicKey; voteAuthorizationType: VoteAuthorizationType; }; /** * 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 an authorize instruction and retrieve the instruction params. */ static decodeAuthorize( instruction: TransactionInstruction, ): AuthorizeVoteParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 3); const {newAuthorized, voteAuthorizationType} = decodeData( VOTE_INSTRUCTION_LAYOUTS.Authorize, instruction.data, ); return { votePubkey: instruction.keys[0].pubkey, authorizedPubkey: instruction.keys[2].pubkey, newAuthorizedPubkey: new PublicKey(newAuthorized), voteAuthorizationType: { index: voteAuthorizationType, }, }; } /** * 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, 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 = // FIXME // 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'; type VoteInstructionInputData = { Authorize: IInstructionInputData & { newAuthorized: Uint8Array; voteAuthorizationType: number; }; InitializeAccount: IInstructionInputData & { voteInit: Readonly<{ authorizedVoter: Uint8Array; authorizedWithdrawer: Uint8Array; commission: number; nodePubkey: Uint8Array; }>; }; Withdraw: IInstructionInputData & { lamports: number; }; }; const VOTE_INSTRUCTION_LAYOUTS = Object.freeze<{ [Instruction in VoteInstructionType]: InstructionType< VoteInstructionInputData[Instruction] >; }>({ InitializeAccount: { index: 0, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), Layout.voteInit(), ]), }, Authorize: { index: 1, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), Layout.publicKey('newAuthorized'), BufferLayout.u32('voteAuthorizationType'), ]), }, Withdraw: { index: 3, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), BufferLayout.ns64('lamports'), ]), }, }); /** * VoteAuthorize type */ export type VoteAuthorizationType = { /** The VoteAuthorize index (from solana-vote-program) */ index: number; }; /** * An enumeration of valid VoteAuthorization layouts. */ export const VoteAuthorizationLayout = Object.freeze({ Voter: { index: 0, }, Withdrawer: { index: 1, }, }); /** * 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 that authorizes a new Voter or Withdrawer on the Vote account. */ static authorize(params: AuthorizeVoteParams): Transaction { const { votePubkey, authorizedPubkey, newAuthorizedPubkey, voteAuthorizationType, } = params; const type = VOTE_INSTRUCTION_LAYOUTS.Authorize; const data = encodeData(type, { newAuthorized: toBuffer(newAuthorizedPubkey.toBuffer()), voteAuthorizationType: voteAuthorizationType.index, }); const keys = [ {pubkey: votePubkey, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ]; return new Transaction().add({ keys, programId: this.programId, data, }); } /** * 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, }); } }