This an SDK for the Staking Protocol. It allows user to stake some tokens in a Pool and claim rewards depending on the duration and weight of their stake.
The Protocol itself creates and interacts with several accounts that refer to different entities used in Staking:
StakePool- account that represents a single Stake Pool to which users will be staking tokens of the specifiedmint;RewardPool- account that represents a single Pool that stores Reward tokens for a givenStakePool;StakeEntry- account that represents a deposit in aStakePool, every stake of tokens creates a newStakeEntry;
Features and Limitations
StakePoolsupports weight and duration configuration:weight of the stake is a multiplier that will be applied to Rewards being given to a user when they claim.
weight of an individual stake can be increased if user stakes tokens for a longer duration (if
StakePoolconfiguration allows it);if
maxWeightof theStakePoolis set to1_000_000_000(equals 1 on-chain) it means that all Stakes regardless of their duration will have the same weight;
user can not unstake tokens before stake duration has passed;
a single
StakePoolcan have up to 10 Reward Pools, each Pool represents a separate mint for tokens that can be claimed as rewards for staking:on every claim user will claim all rewards available to them in the
RewardPool(accounting for reward distribution if there are multiple stakers), meaning that the period of claiming should be controlled by periodic top up for eachRewardPool;whenever user claims tokens, the current state of each
RewardPoolwill be written toclaimedAmountsof theStakeEntry, so user won't be able to claim more tokens unless Reward Pool is topped up with more funds;
for every
StakePoolthere is astakeMintthat is a PDA and represents a staking token that will be minted when user stakes their tokens into the Pool:minted tokens will account for user's weight of the stake;
these tokens may be used in SPL governance;
these tokens will be burnt when user calls
unstake;
Usage
Initiate the client
Every interaction with a Staking Protocol should be done via StakingClient that provides higher level abstraction on top of anchor client generated by anchor-client-gen.
import { Keypair, PublicKey, Connection } from "@solana/web3.js"; import { checkOrCreateAtaBatch, createAtaBatch, getStakeEntryPda, getStakePoolPda, signAndExecuteTransaction, sleep, STAKING_PROGRAM_ID, StakingClient } from "staking"; const url = 'CLUSTER_URL' const client = new StakingClient( { clusterUrl: url, cluster: "devnet" } );Create Staking Pool
Use create method to create a staking pool:
const { stakePool, txId } = await client.create( { mint: MINT, nonce: NONCE, maxWeight: new BN(1_000_000_000), minDuration: new BN(3600), maxDuration: new BN(3600) }, { invoker: sender } );Where:
mint- mint address of tokens to be staked;nonce- nonce value, it will be used to derive Staking Pool PDA, PDA consists of invoker+nonce+mint, so for every new Staking Pool created from the same wallet for the same mint should be done with a differentnoncevalue;maxWeight- max weight of the Stake, weight is increased depending on the duration of the vesting, should be more than1_000_000_000-1_000_000_000stands for 1, we use extra 9 zeroes for precision for on-chain calculations;minDuration/maxDuration- min and max duration of Staking in seconds;when staking tokens user will be able to set
durationof the staking, it should be inmin/maxrange;users won't be able to unstake until
durationhas passed;depending on weight/duration configuration (if
maxWeightis more than1andminDuration!==maxDuration) user may receive more rewards if they stake for a longer time;
invoker- Keypair/Wallet of the sender
Add Reward Pool
Reward Pool stores reward tokens that users will be able to claim if they stake tokens. Each StakePool can have up to 10 Reward Pools.
// In case you didn't store stakePool address you can derive it const stakePoolPubKey = getStakePoolPda(new PublicKey(STAKING_PROGRAM_ID["devnet"]), new PublicKey(MINT), sender.publicKey, NONCE); const { txId } = await client.addRewardPool({ stakePool: stakePoolPubKey.toString(), mint: MINT }, { invoker: sender });Where:
stakePool- address of the stake pool;mint- reward mint token address;invoker- Keypair/Wallet of the authority;
Stake
const { txId } = await client.stake({ stakePool, amount: new BN(1_000_000_000), duration: new BN(3600), nonce: 0 }, { invoker: sender });Where:
stakePool- address of the stake pool;amount- amount of the tokens to stake, RAW AMOUNT, meaning that it should account for how many decimals the MINT has;duration- duration of the staking in seconds;nonce- nonce value, it will be used to derive Deposit Receipt PDA, sender+stakePool+nonce should be unique;
Claim
Claiming is possible anytime after deposit has been made
await client.claim({ stakePool, stakeEntry: getStakeEntryPda(PROGRAM_ID, stakePoolPubKey, sender.publicKey, 0).toString() }, { invoker: sender });Where:
stakePool- address of the stake pool;stakeEntryaddress of the account that stores metadata for the individual Stake, can be derived withgetStakeEntryPdafunction that acceptsPublicKey of the Program
PublicKey of the StakePool
PublicKey of the Staker
Nonce value
Unstake
Unstaking is possible only after staking duration passed
await client.unstake({ stakePool, stakeEntry: getStakeEntryPda(PROGRAM_ID, stakePoolPubKey, sender.publicKey, 0).toString() }, { invoker: sender });Where:
stakePool- address of the stake pool;stakeEntryaddress of the account that stores metadata for the individual Stake, can be derived withgetStakeEntryPdafunction that acceptsPublicKey of the Program
PublicKey of the StakePool
PublicKey of the Staker
Nonce value
ATAs
Both Claim and Unstake expect all ATAs of the invoker to be populated for Reward Pool distribution. ATAs can be created like so:
const stakePoolPubKey = getStakePoolPda(new PublicKey(STAKING_PROGRAM_ID["devnet"]), new PublicKey(MINT), sender.publicKey, NONCE); const stakePool = await client.getStakePool(stakePoolPubKey.toString()); const rewardPools = stakePool.rewardPools .map(({ rewardVault }) => rewardVault) .filter((rewardVault) => !rewardVault.equals(PublicKey.default)); const rewardPoolAccounts = await getMultipleAccounts(client.connection, rewardPools); await createAtaBatch(client.connection, rewardPoolAccounts.map((item) => ({ mint: item!.mint, owner: sender.publicKey })), { invoker: sender }, {}, {});Querying
StakingClient also exposes several methods to query on-chain data, stake pools and entries.
use
getStakePoolto fetch individualStakePoolby its address;use
searchStakeEntriesto fetch entries for a specific pool owner by a specificowner;
const stakePool = await client.getStakePool(stakePoolPubKey.toString()); const stakeEntries = await client.searchStakeEntries({ stakePool: stakePoolPubKey.toString(), owner: sender.publicKey.toString() });Something we didn't cover?
We've tried to cover as much as possible in this guide, but there is always room for improvement. If we missed something, or you'd like to simply share your ideas, love, and support, email us at [email protected]
