Skip to content

Commit

Permalink
stake-pool: supporting AddValidatorToPool in js library (#6459)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaplanmaxe authored Mar 19, 2024
1 parent c609163 commit a62d33a
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 3 deletions.
49 changes: 49 additions & 0 deletions stake-pool/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
58 changes: 57 additions & 1 deletion stake-pool/js/src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export type StakePoolInstructionType =
| 'IncreaseAdditionalValidatorStake'
| 'DecreaseAdditionalValidatorStake'
| 'DecreaseValidatorStakeWithReserve'
| 'Redelegate';
| 'Redelegate'
| 'AddValidatorToPool';

// 'UpdateTokenMetadata' and 'CreateTokenMetadata' have dynamic layouts

Expand Down Expand Up @@ -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<any>([BufferLayout.u8('instruction'), BufferLayout.u32('seed')]),
},
DecreaseValidatorStake: {
index: 3,
layout: MOVE_STAKE_LAYOUT,
Expand Down Expand Up @@ -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.
*/
Expand Down
7 changes: 6 additions & 1 deletion stake-pool/js/src/utils/program-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions stake-pool/js/src/utils/stake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export async function getValidatorListAccount(connection: Connection, pubkey: Pu
if (!account) {
throw new Error('Invalid validator list account');
}

return {
pubkey,
account: {
Expand Down
83 changes: 82 additions & 1 deletion stake-pool/js/test/instructions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +31,7 @@ import {
createPoolTokenMetadata,
updatePoolTokenMetadata,
tokenMetadataLayout,
addValidatorToPool,
} from '../src';

import { decodeData } from '../src/utils';
Expand All @@ -42,6 +45,7 @@ import {
CONSTANTS,
stakeAccountData,
uninitializedStakeAccount,
validatorListMock,
} from './mocks';

describe('StakePoolProgram', () => {
Expand All @@ -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,
Expand Down Expand Up @@ -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 <AccountInfo<any>>{
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;
Expand Down

0 comments on commit a62d33a

Please sign in to comment.