UnStake
info
We highly recommend use our SDK, so we could support you better in case of some problems. Also, integration with SDK is much easier & more simple than manually.
Step 1: Fetching Lido program state to retrieve relevant data
const accountInfo = await connection.getAccountInfo(LIDO_ADDRESS);
Use getAccountInfo(LIDO_ADDRESS) function from this library to fetch the Lido program data (in the form of a buffer).
- Create a borsh schema to deserialize the solido state data:
class Lido {
constructor(data) {
Object.assign(this, data);
}
}
class RewardDistribution {
constructor(data) {
Object.assign(this, data);
}
}
class FeeRecipients {
constructor(data) {
Object.assign(this, data);
}
}
class ExchangeRate {
constructor(data) {
Object.assign(this, data);
}
}
class Metrics {
constructor(data) {
Object.assign(this, data);
}
}
class LamportsHistogram {
constructor(data) {
Object.assign(this, data);
}
}
class WithdrawMetric {
constructor(data) {
Object.assign(this, data);
}
}
const accountInfoScheme = new Map([
[
ExchangeRate,
{
kind: 'struct',
fields: [
['computed_in_epoch', 'u64'],
['st_sol_supply', 'u64'],
['sol_balance', 'u64'],
],
},
],
[
LamportsHistogram,
{
kind: 'struct',
fields: [
['counts1', 'u64'],
['counts2', 'u64'],
['counts3', 'u64'],
['counts4', 'u64'],
['counts5', 'u64'],
['counts6', 'u64'],
['counts7', 'u64'],
['counts8', 'u64'],
['counts9', 'u64'],
['counts10', 'u64'],
['counts11', 'u64'],
['counts12', 'u64'],
['total', 'u64'],
],
},
],
[
WithdrawMetric,
{
kind: 'struct',
fields: [
['total_st_sol_amount', 'u64'],
['total_sol_amount', 'u64'],
['count', 'u64'],
],
},
],
[
Metrics,
{
kind: 'struct',
fields: [
['fee_treasury_sol_total', 'u64'],
['fee_validation_sol_total', 'u64'],
['fee_developer_sol_total', 'u64'],
['st_sol_appreciation_sol_total', 'u64'],
['fee_treasury_st_sol_total', 'u64'],
['fee_validation_st_sol_total', 'u64'],
['fee_developer_st_sol_total', 'u64'],
['deposit_amount', LamportsHistogram],
['withdraw_amount', WithdrawMetric],
],
},
],
[
RewardDistribution,
{
kind: 'struct',
fields: [
['treasury_fee', 'u32'],
['developer_fee', 'u32'],
['st_sol_appreciation', 'u32'],
],
},
],
[
FeeRecipients,
{
kind: 'struct',
fields: [
['treasury_account', [32]],
['developer_account', [32]],
],
},
],
[
Lido,
{
kind: 'struct',
fields: [
['account_type', 'u8'],
['lido_version', 'u8'],
['manager', [32]],
['st_sol_mint', [32]],
['exchange_rate', ExchangeRate],
['sol_reserve_authority_bump_seed', 'u8'],
['stake_authority_bump_seed', 'u8'],
['mint_authority_bump_seed', 'u8'],
['reward_distribution', RewardDistribution],
['fee_recipients', FeeRecipients],
['metrics', Metrics],
['validator_list', [32]],
['maintainer_list', [32]],
['max_commission_percentage', 'u8'],
],
},
],
]);
- Deserialize the data using above schema:
import { deserializeUnchecked } from 'borsh';
// It deserializes object from bytes using schema, without checking the length read
const deserializedAccountInfo = deserializeUnchecked(
accountInfoschema,
Lido,
accountInfo.data,
);
Step 2: Fetching Validators list account
const validatorsList = new PublicKey(deserializedAccountInfo.validators_list);
const validators = await connection.getAccountInfo(validatorsList);
- Create a borsh schema to deserialize the validators data:
class ListHeader {
constructor(data) {
Object.assign(this, data);
}
}
class SeedRange {
constructor(data) {
Object.assign(this, data);
}
}
class ValidatorClass {
constructor(data) {
Object.assign(this, data);
}
}
class AccountList {
constructor(data) {
Object.assign(this, data);
}
}
const validatorsSchema = new Map([
[
ListHeader,
{
kind: 'struct',
fields: [
['account_type', 'u8'],
['lido_version', 'u8'],
['max_entries', 'u32'],
],
},
],
[
SeedRange,
{
kind: 'struct',
fields: [
['begin', 'u64'],
['end', 'u64'],
],
},
],
[
ValidatorClass,
{
kind: 'struct',
fields: [
['vote_account_address', [32]],
['stake_seeds', SeedRange],
['unstake_seeds', SeedRange],
['stake_accounts_balance', 'u64'],
['unstake_accounts_balance', 'u64'],
['effective_stake_balance', 'u64'],
['active', 'u8'],
],
},
],
[
AccountList,
{
kind: 'struct',
fields: [
['header', ListHeader],
['entries', [ValidatorClass]],
],
},
],
]);
- Deserialize the data using above schema:
const deserializedValidators = deserializeUnchecked(
validatorsSchema,
AccountList,
validators.data,
);
Step 3: Sign new Transaction
const newStakeAccount = Keypair.generate();
// Create new transaction
const transaction = new Transaction({ feePayer: payer });
// Set recent blockhash
const { blockhash } = await connection.getLatestBlockhash();
transaction.recentBlockhash = blockhash;
Step 4: Create Withdraw Instruction
- Create the buffer layout in the format of
{ instruction_code: 1 byte, amount: 8 bytes, validator_index: 4 bytes}
:
import { nu64, struct, u32, u8 } from 'buffer-layout';
const dataLayout = struct([u8('instruction'), nu64('amount'), u32('validator_index')]);
const data = Buffer.alloc(dataLayout.span);
- Get heaviest validator index:
const getHeaviestValidator = (validatorEntries) => {
const sortedValidators = validatorEntries.sort(
(validatorA, validatorB) =>
validatorB.stake_accounts_balance.toNumber() - validatorA.stake_accounts_balance.toNumber(),
);
return sortedValidators[0];
};
const getValidatorIndex = (validatorEntries, validator) =>
validatorEntries.findIndex(
({ vote_account_address: voteAccountAddress }) =>
voteAccountAddress.toString() === validator.vote_account_address.toString(),
);
const validator = getHeaviestValidator(validators);
const validatorIndex = getValidatorIndex(validators, validator);
- Encode the deposit data using the buffer layout:
import BN from 'bn.js';
dataLayout.encode(
{
instruction: 23, // code of withdraw instruction
amount: new BN(amount),
validator_index: validatorIndex,
},
data,
);
- Set all keys for the deposit instruction using the program data we fetch earlier:
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
Keypair,
PublicKey,
StakeProgram,
SystemProgram,
SYSVAR_CLOCK_PUBKEY,
} from '@solana/web3.js';
const calculateStakeAuthority = async (lidoAddress, programId) => {
const bufferArray = [lidoAddress.toBuffer(), Buffer.from('stake_authority')];
const mint = await PublicKey.findProgramAddress(bufferArray, programId);
return mint[0];
};
const calculateStakeAccountAddress = async (lidoAddress, programId, validatorVoteAccount, seed) => {
const bufferArray = [
lidoAddress.toBuffer(),
validatorVoteAccount.toBuffer(),
Buffer.from('validator_stake_account'),
seed.toArray('le', 8),
];
const [stakeAccountAddress] = await PublicKey.findProgramAddress(bufferArray, programId);
return stakeAccountAddress;
};
const getStSolAccountsForUser = async (owner) => {
const stSolAccounts = [];
const { value } = await connection.getParsedTokenAccountsByOwner(owner, {
mint: ST_SOL_MINT,
});
value.forEach((v) => {
const address = v.pubkey;
const balanceInLamports = parseInt(v.account.data.parsed?.info?.tokenAmount?.amount ?? '0', 10);
stSolAccounts.push({ address, balanceInLamports });
});
return stSolAccounts;
};
const stakeAuthority = await calculateStakeAuthority(lidoAddress, programId);
const validator = await getHeaviestValidator(validators);
const senderStSolAccountAddress = await getStSolAccountsForUser(wallet.publicKey); // payerAddress
const validatorStakeAccount = await calculateStakeAccountAddress(
LIDO_ADDRESS,
PROGRAM_ID,
new PublicKey(validator.vote_account_address),
validator.stake_seeds.begin,
);
const keys = [
{ pubkey: solidoInstanceId, isSigner: false, isWritable: true },
{ pubkey: payerAddress, isSigner: true, isWritable: false },
{ pubkey: senderStSolAccountAddress, isSigner: false, isWritable: true },
{ pubkey: ST_SOL_MINT, isSigner: false, isWritable: true },
{ pubkey: new PublicKey(validator.vote_account_address), isSigner: false, isWritable: false },
{ pubkey: validatorStakeAccount, isSigner: false, isWritable: true },
{ pubkey: stakeAccount, isSigner: true, isWritable: true },
{ pubkey: stakeAuthority, isSigner: false, isWritable: false },
{ pubkey: validatorsList, isSigner: false, isWritable: true }, // step 2
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: StakeProgram.programId, isSigner: false, isWritable: false },
];
- Add the instruction to the transaction:
transaction.add(
new TransactionInstruction({
keys,
programId: PROGRAM_ID,
data,
}),
);
Step 5: Create deactivate transaction & add its instructions to transaction
import { StakeProgram } from '@solana/web3.js';
const deactivateTransaction = StakeProgram.deactivate({
authorizedPubkey: payer,
stakePubkey: newStakeAccount.publicKey,
});
transaction.add(...deactivateTransaction.instructions);
transaction.partialSign(newStakeAccount); // step 2 variables