stake-pool: add redelegate js bindings (#3960)

* - add ts/js binding for redelegate functionality

* - add redelegate instructions

* - refactor

* - force rebuild

* - refactor

* - force rebuild

* - force rebuild
This commit is contained in:
Alexander Ray 2023-01-12 19:56:06 +01:00 committed by GitHub
parent b33bcc055e
commit 6ab15b340e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 434 additions and 30 deletions

View File

@ -7,6 +7,9 @@ export const STAKE_POOL_PROGRAM_ID = new PublicKey('SPoo1Ku8WFXoNDMHPsrGSTSG1Y47
// Maximum number of validators to update during UpdateValidatorListBalance.
export const MAX_VALIDATORS_TO_UPDATE = 5;
// Seed for ephemeral stake account
export const EPHEMERAL_STAKE_SEED_PREFIX = Buffer.from('ephemeral');
// Seed used to derive transient stake accounts.
export const TRANSIENT_STAKE_SEED_PREFIX = Buffer.from('transient');

View File

@ -24,6 +24,7 @@ import {
prepareWithdrawAccounts,
lamportsToSol,
solToLamports,
findEphemeralStakeProgramAddress,
} from './utils';
import { StakePoolInstruction } from './instructions';
import {
@ -36,6 +37,7 @@ import {
} from './layouts';
import { MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE, STAKE_POOL_PROGRAM_ID } from './constants';
import { create } from 'superstruct';
import BN from 'bn.js';
export type { StakePool, AccountType, ValidatorList, ValidatorStakeInfo } from './layouts';
export { STAKE_POOL_PROGRAM_ID } from './constants';
@ -66,6 +68,17 @@ export interface StakePoolAccounts {
validatorList: ValidatorListAccount | undefined;
}
interface RedelegateProps {
connection: Connection;
stakePoolAddress: PublicKey;
sourceVoteAccount: PublicKey;
destinationVoteAccount: PublicKey;
sourceTransientStakeSeed: number | BN;
destinationTransientStakeSeed: number | BN;
ephemeralStakeSeed: number | BN;
lamports: number | BN;
}
/**
* Retrieves and deserializes a StakePool account using a web3js connection and the stake pool address.
* @param connection: An active web3js connection.
@ -668,6 +681,7 @@ export async function increaseValidatorStake(
stakePoolAddress: PublicKey,
validatorVote: PublicKey,
lamports: number,
ephemeralStakeSeed?: number,
) {
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
@ -705,6 +719,28 @@ export async function increaseValidatorStake(
);
const instructions: TransactionInstruction[] = [];
if (ephemeralStakeSeed != undefined) {
const ephemeralStake = await findEphemeralStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
new BN(ephemeralStakeSeed),
);
StakePoolInstruction.increaseAdditionalValidatorStake({
stakePool: stakePoolAddress,
staker: stakePool.account.data.staker,
validatorList: stakePool.account.data.validatorList,
reserveStake: stakePool.account.data.reserveStake,
transientStakeSeed: transientStakeSeed.toNumber(),
withdrawAuthority,
transientStake,
validatorStake,
validatorVote,
lamports,
ephemeralStake,
ephemeralStakeSeed,
});
} else {
instructions.push(
StakePoolInstruction.increaseValidatorStake({
stakePool: stakePoolAddress,
@ -719,6 +755,7 @@ export async function increaseValidatorStake(
lamports,
}),
);
}
return {
instructions,
@ -733,6 +770,7 @@ export async function decreaseValidatorStake(
stakePoolAddress: PublicKey,
validatorVote: PublicKey,
lamports: number,
ephemeralStakeSeed?: number,
) {
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
const validatorList = await getValidatorListAccount(
@ -769,6 +807,28 @@ export async function decreaseValidatorStake(
);
const instructions: TransactionInstruction[] = [];
if (ephemeralStakeSeed != undefined) {
const ephemeralStake = await findEphemeralStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
new BN(ephemeralStakeSeed),
);
instructions.push(
StakePoolInstruction.decreaseAdditionalValidatorStake({
stakePool: stakePoolAddress,
staker: stakePool.account.data.staker,
validatorList: stakePool.account.data.validatorList,
transientStakeSeed: transientStakeSeed.toNumber(),
withdrawAuthority,
validatorStake,
transientStake,
lamports,
ephemeralStake,
ephemeralStakeSeed,
}),
);
} else {
instructions.push(
StakePoolInstruction.decreaseValidatorStake({
stakePool: stakePoolAddress,
@ -781,6 +841,7 @@ export async function decreaseValidatorStake(
lamports,
}),
);
}
return {
instructions,
@ -993,3 +1054,82 @@ export async function stakePoolInfo(connection: Connection, stakePoolAddress: Pu
}, // CliStakePoolDetails
};
}
/**
* Creates instructions required to redelegate stake.
*/
export async function redelegate(props: RedelegateProps) {
const {
connection,
stakePoolAddress,
sourceVoteAccount,
sourceTransientStakeSeed,
destinationVoteAccount,
destinationTransientStakeSeed,
ephemeralStakeSeed,
lamports,
} = props;
const stakePool = await getStakePoolAccount(connection, stakePoolAddress);
const stakePoolWithdrawAuthority = await findWithdrawAuthorityProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
);
const sourceValidatorStake = await findStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
sourceVoteAccount,
stakePoolAddress,
);
const sourceTransientStake = await findTransientStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
sourceVoteAccount,
stakePoolAddress,
new BN(sourceTransientStakeSeed),
);
const destinationValidatorStake = await findStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
destinationVoteAccount,
stakePoolAddress,
);
const destinationTransientStake = await findTransientStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
destinationVoteAccount,
stakePoolAddress,
new BN(destinationTransientStakeSeed),
);
const ephemeralStake = await findEphemeralStakeProgramAddress(
STAKE_POOL_PROGRAM_ID,
stakePoolAddress,
new BN(ephemeralStakeSeed),
);
const instructions: TransactionInstruction[] = [];
instructions.push(
StakePoolInstruction.redelegate({
stakePool: stakePool.pubkey,
staker: stakePool.account.data.staker,
validatorList: stakePool.account.data.validatorList,
stakePoolWithdrawAuthority,
ephemeralStake,
ephemeralStakeSeed,
sourceValidatorStake,
sourceTransientStake,
sourceTransientStakeSeed,
destinationValidatorStake,
destinationTransientStake,
destinationTransientStakeSeed,
validator: destinationVoteAccount,
lamports,
}),
);
return {
instructions,
};
}

View File

@ -12,6 +12,7 @@ import * as BufferLayout from '@solana/buffer-layout';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { STAKE_POOL_PROGRAM_ID } from './constants';
import { InstructionType, encodeData, decodeData } from './utils';
import BN from 'bn.js';
/**
* An enumeration of valid StakePoolInstructionType's
@ -25,7 +26,10 @@ export type StakePoolInstructionType =
| 'DepositStake'
| 'DepositSol'
| 'WithdrawStake'
| 'WithdrawSol';
| 'WithdrawSol'
| 'IncreaseAdditionalValidatorStake'
| 'DecreaseAdditionalValidatorStake'
| 'Redelegate';
const MOVE_STAKE_LAYOUT = BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
@ -96,6 +100,40 @@ export const STAKE_POOL_INSTRUCTION_LAYOUTS: {
BufferLayout.ns64('poolTokens'),
]),
},
IncreaseAdditionalValidatorStake: {
index: 19,
layout: BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
BufferLayout.ns64('lamports'),
BufferLayout.ns64('transientStakeSeed'),
BufferLayout.ns64('ephemeralStakeSeed'),
]),
},
DecreaseAdditionalValidatorStake: {
index: 20,
layout: BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
BufferLayout.ns64('lamports'),
BufferLayout.ns64('transientStakeSeed'),
BufferLayout.ns64('ephemeralStakeSeed'),
]),
},
Redelegate: {
index: 21,
layout: BufferLayout.struct<any>([
BufferLayout.u8('instruction'),
/// Amount of lamports to redelegate
BufferLayout.ns64('lamports'),
/// Seed used to create source transient stake account
BufferLayout.ns64('sourceTransientStakeSeed'),
/// Seed used to create destination ephemeral account.
BufferLayout.ns64('ephemeralStakeSeed'),
/// Seed used to create destination transient stake account. If there is
/// already transient stake, this must match the current seed, otherwise
/// it can be anything
BufferLayout.ns64('destinationTransientStakeSeed'),
]),
},
});
/**
@ -141,12 +179,17 @@ export type DecreaseValidatorStakeParams = {
validatorList: PublicKey;
validatorStake: PublicKey;
transientStake: PublicKey;
// Amount of lamports to split into the transient stake account.
// Amount of lamports to split into the transient stake account
lamports: number;
// Seed to used to create the transient stake account.
// Seed to used to create the transient stake account
transientStakeSeed: number;
};
export interface DecreaseAdditionalValidatorStakeParams extends DecreaseValidatorStakeParams {
ephemeralStake: PublicKey;
ephemeralStakeSeed: number;
}
/**
* (Staker only) Increase stake on a validator from the reserve account.
*/
@ -159,12 +202,17 @@ export type IncreaseValidatorStakeParams = {
transientStake: PublicKey;
validatorStake: PublicKey;
validatorVote: PublicKey;
// Amount of lamports to split into the transient stake account.
// Amount of lamports to split into the transient stake account
lamports: number;
// Seed to used to create the transient stake account.
// Seed to used to create the transient stake account
transientStakeSeed: number;
};
export interface IncreaseAdditionalValidatorStakeParams extends IncreaseValidatorStakeParams {
ephemeralStake: PublicKey;
ephemeralStakeSeed: number;
}
/**
* Deposits a stake account into the pool in exchange for pool tokens
*/
@ -232,6 +280,29 @@ export type DepositSolParams = {
lamports: number;
};
export type RedelegateParams = {
stakePool: PublicKey;
staker: PublicKey;
stakePoolWithdrawAuthority: PublicKey;
validatorList: PublicKey;
sourceValidatorStake: PublicKey;
sourceTransientStake: PublicKey;
ephemeralStake: PublicKey;
destinationTransientStake: PublicKey;
destinationValidatorStake: PublicKey;
validator: PublicKey;
// Amount of lamports to redelegate
lamports: number | BN;
// Seed used to create source transient stake account
sourceTransientStakeSeed: number | BN;
// Seed used to create destination ephemeral account
ephemeralStakeSeed: number | BN;
// Seed used to create destination transient stake account. If there is
// already transient stake, this must match the current seed, otherwise
// it can be anything
destinationTransientStakeSeed: number | BN;
};
/**
* Stake Pool Instruction class
*/
@ -334,7 +405,8 @@ export class StakePoolInstruction {
}
/**
* Creates instruction to increase the stake on a validator.
* Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to
* transient account)
*/
static increaseValidatorStake(params: IncreaseValidatorStakeParams): TransactionInstruction {
const {
@ -378,7 +450,57 @@ export class StakePoolInstruction {
}
/**
* Creates instruction to decrease the stake on a validator.
* Creates `IncreaseAdditionalValidatorStake` instruction (rebalance from reserve account to
* transient account)
*/
static increaseAdditionalValidatorStake(
params: IncreaseAdditionalValidatorStakeParams,
): TransactionInstruction {
const {
stakePool,
staker,
withdrawAuthority,
validatorList,
reserveStake,
transientStake,
validatorStake,
validatorVote,
lamports,
transientStakeSeed,
ephemeralStake,
ephemeralStakeSeed,
} = params;
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.IncreaseAdditionalValidatorStake;
const data = encodeData(type, { lamports, transientStakeSeed, ephemeralStakeSeed });
const keys = [
{ pubkey: stakePool, isSigner: false, isWritable: false },
{ pubkey: staker, isSigner: true, isWritable: false },
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
{ pubkey: validatorList, isSigner: false, isWritable: true },
{ pubkey: reserveStake, isSigner: false, isWritable: true },
{ pubkey: ephemeralStake, isSigner: false, isWritable: true },
{ pubkey: transientStake, isSigner: false, isWritable: true },
{ pubkey: validatorStake, isSigner: false, isWritable: false },
{ pubkey: validatorVote, 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 `DecreaseValidatorStake` instruction (rebalance from validator account to
* transient account)
*/
static decreaseValidatorStake(params: DecreaseValidatorStakeParams): TransactionInstruction {
const {
@ -415,6 +537,50 @@ export class StakePoolInstruction {
});
}
/**
* Creates `DecreaseAdditionalValidatorStake` instruction (rebalance from
* validator account to transient account)
*/
static decreaseAdditionalValidatorStake(
params: DecreaseAdditionalValidatorStakeParams,
): TransactionInstruction {
const {
stakePool,
staker,
withdrawAuthority,
validatorList,
validatorStake,
transientStake,
lamports,
transientStakeSeed,
ephemeralStakeSeed,
ephemeralStake,
} = params;
const type = STAKE_POOL_INSTRUCTION_LAYOUTS.DecreaseAdditionalValidatorStake;
const data = encodeData(type, { lamports, transientStakeSeed, ephemeralStakeSeed });
const keys = [
{ pubkey: stakePool, isSigner: false, isWritable: false },
{ pubkey: staker, isSigner: true, isWritable: false },
{ pubkey: withdrawAuthority, isSigner: false, isWritable: false },
{ pubkey: validatorList, isSigner: false, isWritable: true },
{ pubkey: validatorStake, isSigner: false, isWritable: true },
{ pubkey: ephemeralStake, isSigner: false, isWritable: true },
{ pubkey: transientStake, isSigner: false, isWritable: true },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_STAKE_HISTORY_PUBKEY, 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 a transaction instruction to deposit a stake account into a stake pool.
*/
@ -603,6 +769,60 @@ export class StakePoolInstruction {
});
}
/**
* Creates `Redelegate` instruction (rebalance from one validator account to another)
* @param params
*/
static redelegate(params: RedelegateParams): TransactionInstruction {
const {
stakePool,
staker,
stakePoolWithdrawAuthority,
validatorList,
sourceValidatorStake,
sourceTransientStake,
ephemeralStake,
destinationTransientStake,
destinationValidatorStake,
validator,
lamports,
sourceTransientStakeSeed,
ephemeralStakeSeed,
destinationTransientStakeSeed,
} = params;
const keys = [
{ pubkey: stakePool, isSigner: false, isWritable: false },
{ pubkey: staker, isSigner: true, isWritable: false },
{ pubkey: stakePoolWithdrawAuthority, isSigner: false, isWritable: false },
{ pubkey: validatorList, isSigner: false, isWritable: true },
{ pubkey: sourceValidatorStake, isSigner: false, isWritable: true },
{ pubkey: sourceTransientStake, isSigner: false, isWritable: true },
{ pubkey: ephemeralStake, isSigner: false, isWritable: true },
{ pubkey: destinationTransientStake, isSigner: false, isWritable: true },
{ pubkey: destinationValidatorStake, isSigner: false, isWritable: false },
{ pubkey: validator, 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 },
];
const data = encodeData(STAKE_POOL_INSTRUCTION_LAYOUTS.Redelegate, {
lamports,
sourceTransientStakeSeed,
ephemeralStakeSeed,
destinationTransientStakeSeed,
});
return new TransactionInstruction({
programId: STAKE_POOL_PROGRAM_ID,
keys,
data,
});
}
/**
* Decode a deposit stake pool instruction and retrieve the instruction params.
*/

View File

@ -1,7 +1,7 @@
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { Buffer } from 'buffer';
import { TRANSIENT_STAKE_SEED_PREFIX } from '../constants';
import { EPHEMERAL_STAKE_SEED_PREFIX, TRANSIENT_STAKE_SEED_PREFIX } from '../constants';
/**
* Generates the withdraw authority program address for the stake pool
@ -46,9 +46,24 @@ export async function findTransientStakeProgramAddress(
TRANSIENT_STAKE_SEED_PREFIX,
voteAccountAddress.toBuffer(),
stakePoolAddress.toBuffer(),
new Uint8Array(seed.toArray('le', 8)),
seed.toBuffer('le', 8),
],
programId,
);
return publicKey;
}
/**
* Generates the ephemeral program address for stake pool redelegation
*/
export async function findEphemeralStakeProgramAddress(
programId: PublicKey,
stakePoolAddress: PublicKey,
seed: BN,
) {
const [publicKey] = await PublicKey.findProgramAddress(
[EPHEMERAL_STAKE_SEED_PREFIX, stakePoolAddress.toBuffer(), seed.toBuffer('le', 8)],
programId,
);
return publicKey;
}

View File

@ -15,6 +15,7 @@ import {
depositSol,
withdrawSol,
withdrawStake,
redelegate,
getStakeAccount,
} from '../src';
@ -317,4 +318,30 @@ describe('StakePoolProgram', () => {
expect(parsedStakeAccount).toEqual(uninitializedStakeAccount.parsed);
});
});
describe('redelegation', () => {
it.only('should call successfully', async () => {
const data = {
connection,
stakePoolAddress,
sourceVoteAccount: PublicKey.default,
sourceTransientStakeSeed: 10,
destinationVoteAccount: PublicKey.default,
destinationTransientStakeSeed: 20,
ephemeralStakeSeed: 100,
lamports: 100,
};
const res = await redelegate(data);
const decodedData = STAKE_POOL_INSTRUCTION_LAYOUTS.Redelegate.layout.decode(
res.instructions[0].data,
);
expect(decodedData.instruction).toBe(21);
expect(decodedData.lamports).toBe(data.lamports);
expect(decodedData.sourceTransientStakeSeed).toBe(data.sourceTransientStakeSeed);
expect(decodedData.destinationTransientStakeSeed).toBe(data.destinationTransientStakeSeed);
expect(decodedData.ephemeralStakeSeed).toBe(data.ephemeralStakeSeed);
});
});
});

View File

@ -1,7 +1,7 @@
import { AccountInfo, LAMPORTS_PER_SOL, PublicKey, StakeProgram } from '@solana/web3.js';
import BN from 'bn.js';
import { ValidatorStakeInfo } from '../src';
import { ValidatorStakeInfoStatus, AccountLayout, ValidatorListLayout } from '../src/layouts';
import { AccountLayout, ValidatorListLayout, ValidatorStakeInfoStatus } from '../src/layouts';
export const CONSTANTS = {
poolTokenAccount: new PublicKey(
@ -149,13 +149,12 @@ export const mockRpc = (data: any): any => {
executable: false,
rentEpoch: 0,
};
const result = {
return {
context: {
slot: 11,
},
value: value,
};
return result;
};
export const stakeAccountData = {