From a62d33a023307404d614bd86192b548028b0b9c3 Mon Sep 17 00:00:00 2001 From: Max Kaplan Date: Tue, 19 Mar 2024 19:18:06 -0400 Subject: [PATCH] stake-pool: supporting AddValidatorToPool in js library (#6459) --- stake-pool/js/src/index.ts | 49 +++++++++++++ stake-pool/js/src/instructions.ts | 58 ++++++++++++++- stake-pool/js/src/utils/program-address.ts | 7 +- stake-pool/js/src/utils/stake.ts | 1 + stake-pool/js/test/instructions.test.ts | 83 +++++++++++++++++++++- 5 files changed, 195 insertions(+), 3 deletions(-) diff --git a/stake-pool/js/src/index.ts b/stake-pool/js/src/index.ts index 307918423e1..4b4981944b5 100644 --- a/stake-pool/js/src/index.ts +++ b/stake-pool/js/src/index.ts @@ -649,6 +649,55 @@ export async function withdrawSol( }; } +export async function addValidatorToPool( + connection: Connection, + stakePoolAddress: PublicKey, + validatorVote: PublicKey, + seed?: number, +) { + const stakePoolAccount = await getStakePoolAccount(connection, stakePoolAddress); + const stakePool = stakePoolAccount.account.data; + const { reserveStake, staker, validatorList } = stakePool; + + const validatorListAccount = await getValidatorListAccount(connection, validatorList); + + const validatorInfo = validatorListAccount.account.data.validators.find( + (v) => v.voteAccountAddress.toBase58() == validatorVote.toBase58(), + ); + + if (validatorInfo) { + throw new Error('Vote account is already in validator list'); + } + + const withdrawAuthority = await findWithdrawAuthorityProgramAddress( + STAKE_POOL_PROGRAM_ID, + stakePoolAddress, + ); + + const validatorStake = await findStakeProgramAddress( + STAKE_POOL_PROGRAM_ID, + validatorVote, + stakePoolAddress, + seed, + ); + + const instructions: TransactionInstruction[] = [ + StakePoolInstruction.addValidatorToPool({ + stakePool: stakePoolAddress, + staker: staker, + reserveStake: reserveStake, + withdrawAuthority: withdrawAuthority, + validatorList: validatorList, + validatorStake: validatorStake, + validatorVote: validatorVote, + }), + ]; + + return { + instructions, + }; +} + /** * Creates instructions required to increase validator stake. */ diff --git a/stake-pool/js/src/instructions.ts b/stake-pool/js/src/instructions.ts index 9891f17b6c7..d758a3c9205 100644 --- a/stake-pool/js/src/instructions.ts +++ b/stake-pool/js/src/instructions.ts @@ -36,7 +36,8 @@ export type StakePoolInstructionType = | 'IncreaseAdditionalValidatorStake' | 'DecreaseAdditionalValidatorStake' | 'DecreaseValidatorStakeWithReserve' - | 'Redelegate'; + | 'Redelegate' + | 'AddValidatorToPool'; // 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts @@ -91,6 +92,10 @@ export function tokenMetadataLayout( export const STAKE_POOL_INSTRUCTION_LAYOUTS: { [type in StakePoolInstructionType]: InstructionType; } = Object.freeze({ + AddValidatorToPool: { + index: 1, + layout: BufferLayout.struct([BufferLayout.u8('instruction'), BufferLayout.u32('seed')]), + }, DecreaseValidatorStake: { index: 3, layout: MOVE_STAKE_LAYOUT, @@ -376,10 +381,61 @@ export type UpdateTokenMetadataParams = { uri: string; }; +export type AddValidatorToPoolParams = { + stakePool: PublicKey; + staker: PublicKey; + reserveStake: PublicKey; + withdrawAuthority: PublicKey; + validatorList: PublicKey; + validatorStake: PublicKey; + validatorVote: PublicKey; + seed?: number; +}; + /** * Stake Pool Instruction class */ export class StakePoolInstruction { + /** + * Creates instruction to add a validator into the stake pool. + */ + static addValidatorToPool(params: AddValidatorToPoolParams): TransactionInstruction { + const { + stakePool, + staker, + reserveStake, + withdrawAuthority, + validatorList, + validatorStake, + validatorVote, + seed, + } = params; + const type = STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool; + const data = encodeData(type, { seed: seed == undefined ? 0 : seed }); + + const keys = [ + { pubkey: stakePool, isSigner: false, isWritable: true }, + { pubkey: staker, isSigner: true, isWritable: false }, + { pubkey: reserveStake, isSigner: false, isWritable: true }, + { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, + { pubkey: validatorList, isSigner: false, isWritable: true }, + { pubkey: validatorStake, isSigner: false, isWritable: true }, + { pubkey: validatorVote, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_RENT_PUBKEY, 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: SystemProgram.programId, isSigner: false, isWritable: false }, + { pubkey: StakeProgram.programId, isSigner: false, isWritable: false }, + ]; + + return new TransactionInstruction({ + programId: STAKE_POOL_PROGRAM_ID, + keys, + data, + }); + } + /** * Creates instruction to update a set of validators in the stake pool. */ diff --git a/stake-pool/js/src/utils/program-address.ts b/stake-pool/js/src/utils/program-address.ts index 5b95ca183e9..827f402113d 100644 --- a/stake-pool/js/src/utils/program-address.ts +++ b/stake-pool/js/src/utils/program-address.ts @@ -28,9 +28,14 @@ export async function findStakeProgramAddress( programId: PublicKey, voteAccountAddress: PublicKey, stakePoolAddress: PublicKey, + seed?: number, ) { const [publicKey] = await PublicKey.findProgramAddress( - [voteAccountAddress.toBuffer(), stakePoolAddress.toBuffer()], + [ + voteAccountAddress.toBuffer(), + stakePoolAddress.toBuffer(), + seed ? new BN(seed).toArrayLike(Buffer, 'le', 4) : Buffer.alloc(0), + ], programId, ); return publicKey; diff --git a/stake-pool/js/src/utils/stake.ts b/stake-pool/js/src/utils/stake.ts index 25535431823..55debbf60a2 100644 --- a/stake-pool/js/src/utils/stake.ts +++ b/stake-pool/js/src/utils/stake.ts @@ -25,6 +25,7 @@ export async function getValidatorListAccount(connection: Connection, pubkey: Pu if (!account) { throw new Error('Invalid validator list account'); } + return { pubkey, account: { diff --git a/stake-pool/js/test/instructions.test.ts b/stake-pool/js/test/instructions.test.ts index 1a39ddffb49..6f4b564cabe 100644 --- a/stake-pool/js/test/instructions.test.ts +++ b/stake-pool/js/test/instructions.test.ts @@ -12,14 +12,16 @@ import { Connection, Keypair, SystemProgram, + StakeProgram, AccountInfo, LAMPORTS_PER_SOL, } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID, TokenAccountNotFoundError } from '@solana/spl-token'; -import { StakePoolLayout } from '../src/layouts'; +import { StakePoolLayout, ValidatorListLayout } from '../src/layouts'; import { STAKE_POOL_INSTRUCTION_LAYOUTS, DepositSolParams, + AddValidatorToPoolParams, StakePoolInstruction, depositSol, withdrawSol, @@ -29,6 +31,7 @@ import { createPoolTokenMetadata, updatePoolTokenMetadata, tokenMetadataLayout, + addValidatorToPool, } from '../src'; import { decodeData } from '../src/utils'; @@ -42,6 +45,7 @@ import { CONSTANTS, stakeAccountData, uninitializedStakeAccount, + validatorListMock, } from './mocks'; describe('StakePoolProgram', () => { @@ -61,6 +65,40 @@ describe('StakePoolProgram', () => { data, }; + it('StakePoolInstruction.addValidatorToPool', () => { + const payload: AddValidatorToPoolParams = { + stakePool: stakePoolAddress, + staker: Keypair.generate().publicKey, + reserveStake: Keypair.generate().publicKey, + withdrawAuthority: Keypair.generate().publicKey, + validatorList: Keypair.generate().publicKey, + validatorStake: Keypair.generate().publicKey, + validatorVote: PublicKey.default, + seed: 0, + }; + + const instruction = StakePoolInstruction.addValidatorToPool(payload); + expect(instruction.keys).toHaveLength(13); + expect(instruction.keys[0].pubkey).toEqual(payload.stakePool); + expect(instruction.keys[1].pubkey).toEqual(payload.staker); + expect(instruction.keys[2].pubkey).toEqual(payload.reserveStake); + expect(instruction.keys[3].pubkey).toEqual(payload.withdrawAuthority); + expect(instruction.keys[4].pubkey).toEqual(payload.validatorList); + expect(instruction.keys[5].pubkey).toEqual(payload.validatorStake); + expect(instruction.keys[6].pubkey).toEqual(payload.validatorVote); + expect(instruction.keys[11].pubkey).toEqual(SystemProgram.programId); + expect(instruction.keys[12].pubkey).toEqual(StakeProgram.programId); + + const decodedData = decodeData( + STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool, + instruction.data, + ); + expect(decodedData.instruction).toEqual( + STAKE_POOL_INSTRUCTION_LAYOUTS.AddValidatorToPool.index, + ); + expect(decodedData.seed).toEqual(payload.seed); + }); + it('StakePoolInstruction.depositSol', () => { const payload: DepositSolParams = { stakePool: stakePoolAddress, @@ -99,6 +137,49 @@ describe('StakePoolProgram', () => { expect(instruction2.keys[10].pubkey).toEqual(payload.depositAuthority); }); + describe('addValidatorToPool', () => { + const validatorList = mockValidatorList(); + const decodedValidatorList = ValidatorListLayout.decode(validatorList.data); + const voteAccount = decodedValidatorList.validators[0].voteAccountAddress; + + it('should throw an error when trying to add an existing validator', async () => { + connection.getAccountInfo = jest.fn(async (pubKey) => { + if (pubKey === stakePoolAddress) { + return stakePoolAccount; + } + return mockValidatorList(); + }); + await expect(addValidatorToPool(connection, stakePoolAddress, voteAccount)).rejects.toThrow( + Error('Vote account is already in validator list'), + ); + }); + + it('should successfully add a validator', async () => { + connection.getAccountInfo = jest.fn(async (pubKey) => { + if (pubKey === stakePoolAddress) { + return stakePoolAccount; + } + return >{ + executable: true, + owner: new PublicKey(0), + lamports: 0, + data, + }; + }); + const res = await addValidatorToPool( + connection, + stakePoolAddress, + validatorListMock.validators[0].voteAccountAddress, + ); + expect((connection.getAccountInfo as jest.Mock).mock.calls.length).toBe(2); + expect(res.instructions).toHaveLength(1); + // Make sure that the validator vote account being added is the one we passed + expect(res.instructions[0].keys[6].pubkey).toEqual( + validatorListMock.validators[0].voteAccountAddress, + ); + }); + }); + describe('depositSol', () => { const from = Keypair.generate().publicKey; const balance = 10000;