// @flow import * as BufferLayout from 'buffer-layout'; import hasha from 'hasha'; import {encodeData} from './instruction'; import type {InstructionType} from './instruction'; import * as Layout from './layout'; import {PublicKey} from './publickey'; import {SystemProgram} from './system-program'; import { SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY, SYSVAR_REWARDS_PUBKEY, SYSVAR_STAKE_HISTORY_PUBKEY, } from './sysvar'; import {Transaction, TransactionInstruction} from './transaction'; import type {TransactionInstructionCtorFields} 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; } } /** * Stake Instruction class */ export class StakeInstruction extends TransactionInstruction { /** * Type of StakeInstruction */ type: InstructionType; constructor(opts?: TransactionInstructionCtorFields, type?: InstructionType) { if ( opts && opts.programId && !opts.programId.equals(StakeProgram.programId) ) { throw new Error('programId incorrect; not a StakeInstruction'); } super(opts); if (type) { this.type = type; } } static from(instruction: TransactionInstruction): StakeInstruction { if (!instruction.programId.equals(StakeProgram.programId)) { throw new Error('programId incorrect; not StakeProgram'); } const instructionTypeLayout = BufferLayout.u32('instruction'); const typeIndex = instructionTypeLayout.decode(instruction.data); let type; for (const t in StakeInstructionLayout) { if (StakeInstructionLayout[t].index == typeIndex) { type = StakeInstructionLayout[t]; } } if (!type) { throw new Error('Instruction type incorrect; not a StakeInstruction'); } return new StakeInstruction( { keys: instruction.keys, programId: instruction.programId, data: instruction.data, }, type, ); } } /** * An enumeration of valid StakeInstructionTypes */ export const StakeInstructionLayout = 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'), ]), }, DelegateStake: { index: 2, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, RedeemVoteCredits: { index: 3, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, Split: { index: 4, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), BufferLayout.ns64('lamports'), ]), }, Withdraw: { index: 5, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), BufferLayout.ns64('lamports'), ]), }, Deactivate: { index: 6, layout: BufferLayout.struct([BufferLayout.u32('instruction')]), }, }); /** * @typedef {Object} StakeAuthorizationType * @property (index} The Stake Authorization index (from solana-stake-program) */ export type StakeAuthorizationType = {| index: number, |}; /** * An enumeration of valid StakeInstructionTypes */ export const StakeAuthorizationLayout = Object.freeze({ Staker: { index: 0, }, Withdrawer: { index: 1, }, }); class RewardsPoolPublicKey extends PublicKey { static get rewardsPoolBaseId(): PublicKey { return new PublicKey('StakeRewards1111111111111111111111111111111'); } /** * Generate Derive a public key from another key, a seed, and a programId. */ static randomId(): PublicKey { const randomInt = Math.floor(Math.random() * (256 - 1)); let pubkey = this.rewardsPoolBaseId; for (let i = 0; i < randomInt; i++) { const buffer = pubkey.toBuffer(); const hash = hasha(buffer, {algorithm: 'sha256'}); return new PublicKey('0x' + hash); } return pubkey; } } /** * 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 */ static get space(): number { return 2008; } /** * Generate an Initialize instruction to add to a Stake Create transaction */ static initialize( stakeAccount: PublicKey, authorized: Authorized, lockup: Lockup, ): TransactionInstruction { const type = StakeInstructionLayout.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: stakeAccount, 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( from: PublicKey, stakeAccount: PublicKey, base: PublicKey, seed: string, authorized: Authorized, lockup: Lockup, lamports: number, ): Transaction { let transaction = SystemProgram.createAccountWithSeed( from, stakeAccount, base, seed, lamports, this.space, this.programId, ); return transaction.add(this.initialize(stakeAccount, authorized, lockup)); } /** * Generate a Transaction that creates a new Stake account */ static createAccount( from: PublicKey, stakeAccount: PublicKey, authorized: Authorized, lockup: Lockup, lamports: number, ): Transaction { let transaction = SystemProgram.createAccount( from, stakeAccount, lamports, this.space, this.programId, ); return transaction.add(this.initialize(stakeAccount, 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( stakeAccount: PublicKey, authorizedPubkey: PublicKey, votePubkey: PublicKey, ): Transaction { const type = StakeInstructionLayout.DelegateStake; const data = encodeData(type); return new Transaction().add({ keys: [ {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: votePubkey, isSigner: false, isWritable: false}, {pubkey: SYSVAR_CLOCK_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( stakeAccount: PublicKey, authorizedPubkey: PublicKey, newAuthorized: PublicKey, stakeAuthorizationType: StakeAuthorizationType, ): Transaction { const type = StakeInstructionLayout.Authorize; const data = encodeData(type, { newAuthorized: newAuthorized.toBuffer(), stakeAuthorizationType: stakeAuthorizationType.index, }); return new Transaction().add({ keys: [ {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: true}, {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 redeemVoteCredits( stakeAccount: PublicKey, votePubkey: PublicKey, ): Transaction { const type = StakeInstructionLayout.RedeemVoteCredits; const data = encodeData(type); return new Transaction().add({ keys: [ {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: votePubkey, isSigner: false, isWritable: true}, { pubkey: RewardsPoolPublicKey.randomId(), isSigner: false, isWritable: true, }, {pubkey: SYSVAR_REWARDS_PUBKEY, isSigner: false, isWritable: false}, { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false, }, ], programId: this.programId, data, }); } /** * Generate a Transaction that splits Stake tokens into another stake account */ static split( stakeAccount: PublicKey, authorizedPubkey: PublicKey, lamports: number, splitStakePubkey: PublicKey, ): Transaction { let transaction = SystemProgram.createAccount( stakeAccount, splitStakePubkey, 0, this.space, this.programId, ); transaction.instructions[0].keys[0].isSigner = false; const type = StakeInstructionLayout.Split; const data = encodeData(type, {lamports}); return transaction.add({ keys: [ {pubkey: stakeAccount, 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( stakeAccount: PublicKey, withdrawerPubkey: PublicKey, to: PublicKey, lamports: number, ): Transaction { const type = StakeInstructionLayout.Withdraw; const data = encodeData(type, {lamports}); return new Transaction().add({ keys: [ {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: to, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, { pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, isSigner: false, isWritable: false, }, {pubkey: withdrawerPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, data, }); } /** * Generate a Transaction that deactivates Stake tokens. */ static deactivate( stakeAccount: PublicKey, authorizedPubkey: PublicKey, ): Transaction { const type = StakeInstructionLayout.Deactivate; const data = encodeData(type); return new Transaction().add({ keys: [ {pubkey: stakeAccount, isSigner: false, isWritable: true}, {pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false}, {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, ], programId: this.programId, data, }); } }