// @flow import * as BufferLayout from 'buffer-layout'; import {encodeData, decodeData} from './instruction'; import * as Layout from './layout'; import {PublicKey} from './publickey'; import {SystemProgram} from './system-program'; import { SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY, } from './sysvar'; import {Transaction, TransactionInstruction} from './transaction'; export const STAKE_CONFIG_ID = new PublicKey( 'StakeConfig11111111111111111111111111111111', ); export class Authorized { staker: PublicKey; withdrawer: PublicKey; /** * Create a new Authorized object */ constructor(staker: PublicKey, withdrawer: PublicKey) { this.staker = staker; this.withdrawer = withdrawer; } } export class Lockup { unixTimestamp: number; epoch: number; custodian: PublicKey; /** * Create a new Lockup object */ constructor(unixTimestamp: number, epoch: number, custodian: PublicKey) { this.unixTimestamp = unixTimestamp; this.epoch = epoch; this.custodian = custodian; } } /** * Create stake account transaction params * @typedef {Object} CreateStakeAccountParams * @property {PublicKey} fromPubkey * @property {PublicKey} stakePubkey * @property {Authorized} authorized * @property {Lockup} lockup * @property {number} lamports */ export type CreateStakeAccountParams = {| fromPubkey: PublicKey, stakePubkey: PublicKey, authorized: Authorized, lockup: Lockup, lamports: number, |}; /** * Create stake account with seed transaction params * @typedef {Object} CreateStakeAccountWithSeedParams * @property {PublicKey} fromPubkey * @property {PublicKey} stakePubkey * @property {PublicKey} basePubkey * @property {string} seed * @property {Authorized} authorized * @property {Lockup} lockup * @property {number} lamports */ export type CreateStakeAccountWithSeedParams = {| fromPubkey: PublicKey, stakePubkey: PublicKey, basePubkey: PublicKey, seed: string, authorized: Authorized, lockup: Lockup, lamports: number, |}; /** * Initialize stake instruction params * @typedef {Object} InitializeStakeParams * @property {PublicKey} stakePubkey * @property {Authorized} authorized * @property {Lockup} lockup */ export type InitializeStakeParams = {| stakePubkey: PublicKey, authorized: Authorized, lockup: Lockup, |}; /** * Delegate stake instruction params * @typedef {Object} DelegateStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorizedPubkey * @property {PublicKey} votePubkey */ export type DelegateStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, votePubkey: PublicKey, |}; /** * Authorize stake instruction params * @typedef {Object} AuthorizeStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorizedPubkey * @property {PublicKey} newAuthorizedPubkey * @property {StakeAuthorizationType} stakeAuthorizationType * @property {PublicKey} custodianPubkey */ export type AuthorizeStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, newAuthorizedPubkey: PublicKey, stakeAuthorizationType: StakeAuthorizationType, custodianPubkey?: PublicKey, |}; /** * Authorize stake instruction params using a derived key * @typedef {Object} AuthorizeWithSeedStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorityBase * @property {string} authoritySeed * @property {PublicKey} authorityOwner * @property {PublicKey} newAuthorizedPubkey * @property {StakeAuthorizationType} stakeAuthorizationType * @property {PublicKey} custodianPubkey */ export type AuthorizeWithSeedStakeParams = {| stakePubkey: PublicKey, authorityBase: PublicKey, authoritySeed: string, authorityOwner: PublicKey, newAuthorizedPubkey: PublicKey, stakeAuthorizationType: StakeAuthorizationType, custodianPubkey?: PublicKey, |}; /** * Split stake instruction params * @typedef {Object} SplitStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorizedPubkey * @property {PublicKey} splitStakePubkey * @property {number} lamports */ export type SplitStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, splitStakePubkey: PublicKey, lamports: number, |}; /** * Withdraw stake instruction params * @typedef {Object} WithdrawStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorizedPubkey * @property {PublicKey} toPubkey * @property {number} lamports * @property {PublicKey} custodianPubkey */ export type WithdrawStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, toPubkey: PublicKey, lamports: number, custodianPubkey?: PublicKey, |}; /** * Deactivate stake instruction params * @typedef {Object} DeactivateStakeParams * @property {PublicKey} stakePubkey * @property {PublicKey} authorizedPubkey */ export type DeactivateStakeParams = {| stakePubkey: PublicKey, authorizedPubkey: PublicKey, |}; /** * Stake Instruction class */ export class StakeInstruction { /** * Decode a stake instruction and retrieve the instruction type. */ static decodeInstructionType( instruction: TransactionInstruction, ): StakeInstructionType { this.checkProgramId(instruction.programId); const instructionTypeLayout = BufferLayout.u32('instruction'); const typeIndex = instructionTypeLayout.decode(instruction.data); let type; for (const t of Object.keys(STAKE_INSTRUCTION_LAYOUTS)) { if (STAKE_INSTRUCTION_LAYOUTS[t].index == typeIndex) { type = t; } } if (!type) { throw new Error('Instruction type incorrect; not a StakeInstruction'); } return type; } /** * Decode a initialize stake instruction and retrieve the instruction params. */ static decodeInitialize( instruction: TransactionInstruction, ): InitializeStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 2); const {authorized, lockup} = decodeData( STAKE_INSTRUCTION_LAYOUTS.Initialize, instruction.data, ); return { stakePubkey: instruction.keys[0].pubkey, authorized: new Authorized( new PublicKey(authorized.staker), new PublicKey(authorized.withdrawer), ), lockup: new Lockup( lockup.unixTimestamp, lockup.epoch, new PublicKey(lockup.custodian), ), }; } /** * Decode a delegate stake instruction and retrieve the instruction params. */ static decodeDelegate( instruction: TransactionInstruction, ): DelegateStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 6); decodeData(STAKE_INSTRUCTION_LAYOUTS.Delegate, instruction.data); return { stakePubkey: instruction.keys[0].pubkey, votePubkey: instruction.keys[1].pubkey, authorizedPubkey: instruction.keys[5].pubkey, }; } /** * Decode an authorize stake instruction and retrieve the instruction params. */ static decodeAuthorize( instruction: TransactionInstruction, ): AuthorizeStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 3); const {newAuthorized, stakeAuthorizationType} = decodeData( STAKE_INSTRUCTION_LAYOUTS.Authorize, instruction.data, ); const o: AuthorizeStakeParams = { stakePubkey: instruction.keys[0].pubkey, authorizedPubkey: instruction.keys[2].pubkey, newAuthorizedPubkey: new PublicKey(newAuthorized), stakeAuthorizationType: { index: stakeAuthorizationType, }, }; if (instruction.keys.length > 3) { o.custodianPubkey = instruction.keys[3].pubkey; } return o; } /** * Decode an authorize-with-seed stake instruction and retrieve the instruction params. */ static decodeAuthorizeWithSeed( instruction: TransactionInstruction, ): AuthorizeWithSeedStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 2); const { newAuthorized, stakeAuthorizationType, authoritySeed, authorityOwner, } = decodeData( STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed, instruction.data, ); const o: AuthorizeWithSeedStakeParams = { stakePubkey: instruction.keys[0].pubkey, authorityBase: instruction.keys[1].pubkey, authoritySeed: authoritySeed, authorityOwner: new PublicKey(authorityOwner), newAuthorizedPubkey: new PublicKey(newAuthorized), stakeAuthorizationType: { index: stakeAuthorizationType, }, }; if (instruction.keys.length > 3) { o.custodianPubkey = instruction.keys[3].pubkey; } return o; } /** * Decode a split stake instruction and retrieve the instruction params. */ static decodeSplit(instruction: TransactionInstruction): SplitStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 3); const {lamports} = decodeData( STAKE_INSTRUCTION_LAYOUTS.Split, instruction.data, ); return { stakePubkey: instruction.keys[0].pubkey, splitStakePubkey: instruction.keys[1].pubkey, authorizedPubkey: instruction.keys[2].pubkey, lamports, }; } /** * Decode a withdraw stake instruction and retrieve the instruction params. */ static decodeWithdraw( instruction: TransactionInstruction, ): WithdrawStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 5); const {lamports} = decodeData( STAKE_INSTRUCTION_LAYOUTS.Withdraw, instruction.data, ); const o: WithdrawStakeParams = { stakePubkey: instruction.keys[0].pubkey, toPubkey: instruction.keys[1].pubkey, authorizedPubkey: instruction.keys[4].pubkey, lamports, }; if (instruction.keys.length > 5) { o.custodianPubkey = instruction.keys[5].pubkey; } return o; } /** * Decode a deactivate stake instruction and retrieve the instruction params. */ static decodeDeactivate( instruction: TransactionInstruction, ): DeactivateStakeParams { this.checkProgramId(instruction.programId); this.checkKeyLength(instruction.keys, 3); decodeData(STAKE_INSTRUCTION_LAYOUTS.Deactivate, instruction.data); return { stakePubkey: instruction.keys[0].pubkey, authorizedPubkey: instruction.keys[2].pubkey, }; } /** * @private */ static checkProgramId(programId: PublicKey) { if (!programId.equals(StakeProgram.programId)) { throw new Error('invalid instruction; programId is not StakeProgram'); } } /** * @private */ 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 StakeInstructionType's * @typedef { 'Initialize' | 'Authorize' | 'AuthorizeWithSeed' | 'Delegate' | 'Split' | 'Withdraw' | 'Deactivate' } StakeInstructionType */ export type StakeInstructionType = $Keys; /** * An enumeration of valid stake InstructionType's */ export const STAKE_INSTRUCTION_LAYOUTS = Object.freeze({ Initialize: { index: 0, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), Layout.authorized(), Layout.lockup(), ]), }, Authorize: { index: 1, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), Layout.publicKey('newAuthorized'), BufferLayout.u32('stakeAuthorizationType'), ]), }, Delegate: { index: 2, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, Split: { index: 3, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), BufferLayout.ns64('lamports'), ]), }, Withdraw: { index: 4, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), BufferLayout.ns64('lamports'), ]), }, Deactivate: { index: 5, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, AuthorizeWithSeed: { index: 8, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), Layout.publicKey('newAuthorized'), BufferLayout.u32('stakeAuthorizationType'), Layout.rustString('authoritySeed'), Layout.publicKey('authorityOwner'), ]), }, }); /** * @typedef {Object} StakeAuthorizationType * @property (index} The Stake Authorization index (from solana-stake-program) */ export type StakeAuthorizationType = {| index: number, |}; /** * An enumeration of valid StakeAuthorizationLayout's */ export const StakeAuthorizationLayout = Object.freeze({ Staker: { index: 0, }, Withdrawer: { index: 1, }, }); /** * Factory class for transactions to interact with the Stake program */ export class StakeProgram { /** * Public key that identifies the Stake program */ static get programId(): PublicKey { return new PublicKey('Stake11111111111111111111111111111111111111'); } /** * Max space of a Stake account * * This is generated from the solana-stake-program StakeState struct as * `std::mem::size_of::()`: * https://docs.rs/solana-stake-program/1.4.4/solana_stake_program/stake_state/enum.StakeState.html */ static get space(): number { return 200; } /** * Generate an Initialize instruction to add to a Stake Create transaction */ static initialize(params: InitializeStakeParams): TransactionInstruction { const {stakePubkey, authorized, lockup} = params; const type = STAKE_INSTRUCTION_LAYOUTS.Initialize; const data = encodeData(type, { authorized: { staker: authorized.staker.toBuffer(), withdrawer: authorized.withdrawer.toBuffer(), }, lockup: { unixTimestamp: lockup.unixTimestamp, epoch: lockup.epoch, custodian: lockup.custodian.toBuffer(), }, }); const instructionData = { keys: [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false}, ], programId: this.programId, data, }; return new TransactionInstruction(instructionData); } /** * Generate a Transaction that creates a new Stake account at * an address generated with `from`, a seed, and the Stake programId */ static createAccountWithSeed( params: CreateStakeAccountWithSeedParams, ): Transaction { const transaction = new Transaction(); transaction.add( SystemProgram.createAccountWithSeed({ fromPubkey: params.fromPubkey, newAccountPubkey: params.stakePubkey, basePubkey: params.basePubkey, seed: params.seed, lamports: params.lamports, space: this.space, programId: this.programId, }), ); const {stakePubkey, authorized, lockup} = params; return transaction.add(this.initialize({stakePubkey, authorized, lockup})); } /** * Generate a Transaction that creates a new Stake account */ static createAccount(params: CreateStakeAccountParams): Transaction { const transaction = new Transaction(); transaction.add( SystemProgram.createAccount({ fromPubkey: params.fromPubkey, newAccountPubkey: params.stakePubkey, lamports: params.lamports, space: this.space, programId: this.programId, }), ); const {stakePubkey, authorized, lockup} = params; return transaction.add(this.initialize({stakePubkey, authorized, lockup})); } /** * Generate a Transaction that delegates Stake tokens to a validator * Vote PublicKey. This transaction can also be used to redelegate Stake * to a new validator Vote PublicKey. */ static delegate(params: DelegateStakeParams): Transaction { const {stakePubkey, authorizedPubkey, votePubkey} = params; const type = STAKE_INSTRUCTION_LAYOUTS.Delegate; const data = encodeData(type); return new Transaction().add({ keys: [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: votePubkey, isSigner: false, isWritable: false}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false, }, {pubkey: STAKE_CONFIG_ID, isSigner: false, isWritable: false}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, data, }); } /** * Generate a Transaction that authorizes a new PublicKey as Staker * or Withdrawer on the Stake account. */ static authorize(params: AuthorizeStakeParams): Transaction { const { stakePubkey, authorizedPubkey, newAuthorizedPubkey, stakeAuthorizationType, custodianPubkey, } = params; const type = STAKE_INSTRUCTION_LAYOUTS.Authorize; const data = encodeData(type, { newAuthorized: newAuthorizedPubkey.toBuffer(), stakeAuthorizationType: stakeAuthorizationType.index, }); const keys = [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: true}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ]; if (custodianPubkey) { keys.push({pubkey: custodianPubkey, isSigner: false, isWritable: false}); } return new Transaction().add({ keys, programId: this.programId, data, }); } /** * Generate a Transaction that authorizes a new PublicKey as Staker * or Withdrawer on the Stake account. */ static authorizeWithSeed(params: AuthorizeWithSeedStakeParams): Transaction { const { stakePubkey, authorityBase, authoritySeed, authorityOwner, newAuthorizedPubkey, stakeAuthorizationType, custodianPubkey, } = params; const type = STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed; const data = encodeData(type, { newAuthorized: newAuthorizedPubkey.toBuffer(), stakeAuthorizationType: stakeAuthorizationType.index, authoritySeed: authoritySeed, authorityOwner: authorityOwner.toBuffer(), }); const keys = [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: authorityBase, isSigner: true, isWritable: false}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, ]; if (custodianPubkey) { keys.push({pubkey: custodianPubkey, isSigner: false, isWritable: false}); } return new Transaction().add({ keys, programId: this.programId, data, }); } /** * Generate a Transaction that splits Stake tokens into another stake account */ static split(params: SplitStakeParams): Transaction { const {stakePubkey, authorizedPubkey, splitStakePubkey, lamports} = params; const transaction = new Transaction(); transaction.add( SystemProgram.createAccount({ fromPubkey: authorizedPubkey, newAccountPubkey: splitStakePubkey, lamports: 0, space: this.space, programId: this.programId, }), ); const type = STAKE_INSTRUCTION_LAYOUTS.Split; const data = encodeData(type, {lamports}); return transaction.add({ keys: [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: splitStakePubkey, isSigner: false, isWritable: true}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, data, }); } /** * Generate a Transaction that withdraws deactivated Stake tokens. */ static withdraw(params: WithdrawStakeParams): Transaction { const { stakePubkey, authorizedPubkey, toPubkey, lamports, custodianPubkey, } = params; const type = STAKE_INSTRUCTION_LAYOUTS.Withdraw; const data = encodeData(type, {lamports}); const keys = [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: toPubkey, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false, }, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ]; if (custodianPubkey) { keys.push({pubkey: custodianPubkey, isSigner: false, isWritable: false}); } return new Transaction().add({ keys, programId: this.programId, data, }); } /** * Generate a Transaction that deactivates Stake tokens. */ static deactivate(params: DeactivateStakeParams): Transaction { const {stakePubkey, authorizedPubkey} = params; const type = STAKE_INSTRUCTION_LAYOUTS.Deactivate; const data = encodeData(type); return new Transaction().add({ keys: [ {pubkey: stakePubkey, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, data, }); } }