@karpeleslab/chiefstaker-sdk
v0.2.1
Published
TypeScript SDK for the ChiefStaker time-weighted staking program on Solana
Readme
@karpeleslab/chiefstaker-sdk
TypeScript SDK for the ChiefStaker time-weighted staking program on Solana.
ChiefStaker lets token holders stake SPL Token or Token 2022 assets and earn SOL rewards distributed proportionally by a time-weighted formula. Newer stakers start with near-zero weight that grows exponentially toward full weight over a configurable time constant (tau).
Program ID: 3Ecf8gyRURyrBtGHS1XAVXyQik5PqgDch4VkxrH4ECcr
Staking UI: https://www.tibane.net/staking/<mint_address>
Installation
npm install @karpeleslab/chiefstaker-sdk @solana/web3.js@solana/web3.js v1.90+ is a peer dependency.
Quick start
import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import { ChiefStakerClient } from "@karpeleslab/chiefstaker-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com");
const client = new ChiefStakerClient(connection);
const mint = new PublicKey("YourTokenMint...");
// Fetch pool state
const pool = await client.getPool(mint);
console.log("Total staked:", pool.totalStaked);
// Fetch your stake
const [poolAddress] = client.findPoolAddress(mint);
const stake = await client.getUserStake(poolAddress, wallet.publicKey);
console.log("My stake:", stake?.amount);Usage
The SDK provides three layers you can use independently:
ChiefStakerClient— high-level class with account fetching, PDA derivation, instruction building, and transaction sending in one place.- Instruction builders — standalone
create*Instruction()functions that return aTransactionInstructionfor composing into your own transactions. - Account helpers —
fetch*()anddeserialize*()functions for reading on-chain state.
Using the client
import { ChiefStakerClient } from "@karpeleslab/chiefstaker-sdk";
const client = new ChiefStakerClient(connection);You can optionally pass a custom program ID as the second argument.
Staking tokens
const tokenProgram = await client.detectTokenProgram(mint);
const [poolAddress] = client.findPoolAddress(mint);
const ix = client.stake({
pool: poolAddress,
mint,
owner: wallet.publicKey,
userTokenAccount: myTokenAccount,
amount: 1_000_000n,
tokenProgram,
});
await client.sendTransaction([ix], [keypair]);The pool's metadata PDA is a required account on the stake, unstake, and close
paths (it keeps member_count exact). The SDK derives and appends it
automatically, so you don't pass it explicitly — an uninitialized metadata
account is tolerated on-chain for pools that never called setPoolMetadata.
Unstaking tokens
Pools can be configured with or without an unstake cooldown. Check
pool.unstakeCooldownSeconds to determine which flow to use.
Direct unstake (when unstakeCooldownSeconds === 0n):
const ix = client.unstake({
pool: poolAddress,
mint,
owner: wallet.publicKey,
userTokenAccount: myTokenAccount,
amount: 500_000n,
tokenProgram,
});Cooldown unstake (when unstakeCooldownSeconds > 0n):
// Step 1: Request unstake (starts the cooldown timer)
const requestIx = client.requestUnstake({
pool: poolAddress,
owner: wallet.publicKey,
amount: 500_000n,
});
// Step 2: After cooldown elapses, complete the unstake
const completeIx = client.completeUnstake({
pool: poolAddress,
mint,
owner: wallet.publicKey,
userTokenAccount: myTokenAccount,
tokenProgram,
});
// Optional: Cancel a pending request (tokens remain staked)
const cancelIx = client.cancelUnstakeRequest({
pool: poolAddress,
owner: wallet.publicKey,
});Checking pending rewards and weight
Query claimable SOL and current staking weight without sending a transaction:
// Get pending claimable rewards in lamports
const rewards = await client.getClaimableRewards(poolAddress, wallet.publicKey);
console.log(`Claimable: ${Number(rewards) / 1e9} SOL`);
// Get current staking weight (0 to 1)
const weight = await client.getStakeWeight(poolAddress, wallet.publicKey);
console.log(`Weight: ${(weight * 100).toFixed(1)}%`);Both methods accept an optional currentTime parameter to simulate a future
timestamp.
Claiming rewards
SOL rewards accrue based on your time-weighted stake. Claim them at any time:
const ix = client.claimRewards({
pool: poolAddress,
owner: wallet.publicKey,
});Depositing rewards
Anyone can deposit SOL rewards into a pool:
const ix = client.depositRewards({
pool: poolAddress,
depositor: wallet.publicKey,
amount: 1_000_000_000n, // 1 SOL in lamports
});Pool administration
These operations require the pool authority to sign.
// Update pool settings
const ix = client.updatePoolSettings({
pool: poolAddress,
authority: authorityKeypair.publicKey,
minStakeAmount: 100_000n, // set minimum stake
lockDurationSeconds: 86400n, // 1 day lock after staking
unstakeCooldownSeconds: 259200n, // 3 day cooldown to unstake
});
// Transfer authority (set to PublicKey.default to renounce irreversibly)
const ix = client.transferAuthority({
pool: poolAddress,
authority: authorityKeypair.publicKey,
newAuthority: newAuthorityPubkey,
});Permissionless cranks
These instructions can be called by anyone and require no special authority:
// Rebase pool to prevent overflow (call periodically)
const syncIx = client.syncPool(poolAddress);
// Sync SOL sent directly to the pool PDA (e.g., from pump.fun fees)
const syncRewardsIx = client.syncRewards(poolAddress);
// Set pool metadata for explorer display
const metaIx = client.setPoolMetadata({
pool: poolAddress,
mint,
payer: wallet.publicKey,
});
// Claim pump.fun fee ownership for the pool
const feeIx = client.takeFeeOwnership({
pool: poolAddress,
mint,
});Staking on behalf of another user
Stake tokens for a beneficiary who doesn't need to sign:
const ix = client.stakeOnBehalf({
pool: poolAddress,
mint,
staker: myKeypair.publicKey, // pays rent and provides tokens
beneficiary: recipientPubkey, // receives the staking position
stakerTokenAccount: myTokenAccount,
amount: 1_000_000n,
tokenProgram,
});Closing a stake account
After fully unstaking and claiming all rewards, reclaim the rent:
const ix = client.closeStakeAccount({
pool: poolAddress,
owner: wallet.publicKey,
});Using with wallet adapters
The client works with any @solana/wallet-adapter compatible wallet:
const ix = client.stake({ /* ... */ });
await client.sendTransactionWithWallet([ix], wallet);Using standalone instruction builders
If you prefer to compose transactions yourself:
import {
createStakeInstruction,
createClaimRewardsInstruction,
} from "@karpeleslab/chiefstaker-sdk";
import { Transaction } from "@solana/web3.js";
const stakeIx = createStakeInstruction({
pool: poolAddress,
mint,
owner: wallet.publicKey,
userTokenAccount: myTokenAccount,
amount: 1_000_000n,
tokenProgram,
});
const claimIx = createClaimRewardsInstruction({
pool: poolAddress,
owner: wallet.publicKey,
});
// Combine into a single transaction
const tx = new Transaction().add(stakeIx, claimIx);Fetching accounts
Single account fetches
import {
fetchStakingPool,
fetchUserStake,
fetchPoolMetadata,
} from "@karpeleslab/chiefstaker-sdk";
// By mint address
const pool = await fetchStakingPool(connection, mint);
// By pool PDA address (if you already know it)
const pool = await client.getPoolByAddress(poolPda);
// User stake
const stake = await fetchUserStake(connection, poolAddress, ownerPubkey);
// Pool metadata
const meta = await fetchPoolMetadata(connection, poolAddress);
console.log(meta.name, meta.tags, meta.memberCount);Batch fetches
import {
fetchAllStakingPools,
fetchUserStakesByPool,
fetchUserStakesByOwner,
} from "@karpeleslab/chiefstaker-sdk";
// All pools in the program
const allPools = await fetchAllStakingPools(connection);
for (const { address, account } of allPools) {
console.log(address.toBase58(), account.totalStaked);
}
// All stakers in a specific pool
const stakers = await fetchUserStakesByPool(connection, poolAddress);
// All of a user's stakes across every pool
const myStakes = await fetchUserStakesByOwner(connection, wallet.publicKey);Raw deserialization
If you already have account data (e.g., from getMultipleAccountsInfo):
import {
deserializeStakingPool,
deserializeUserStake,
deserializePoolMetadata,
} from "@karpeleslab/chiefstaker-sdk";
const accountInfo = await connection.getAccountInfo(poolAddress);
const pool = deserializeStakingPool(accountInfo.data);PDA derivation
All PDAs can be derived without network calls:
import {
findPoolAddress,
findTokenVaultAddress,
findUserStakeAddress,
findPoolMetadataAddress,
} from "@karpeleslab/chiefstaker-sdk";
const [pool, poolBump] = findPoolAddress(mint); // ["pool", mint]
const [vault, vaultBump] = findTokenVaultAddress(pool); // ["token_vault", pool]
const [stake, stakeBump] = findUserStakeAddress(pool, owner); // ["stake", pool, owner]
const [meta, metaBump] = findPoolMetadataAddress(pool); // ["metadata", pool]Additional PDAs for pump.fun fee integration:
import {
findSharingConfig,
findBondingCurve,
findPumpCreatorVault,
findCoinCreatorVaultAuth,
} from "@karpeleslab/chiefstaker-sdk";
const [sharingConfig] = findSharingConfig(mint);
const [bondingCurve] = findBondingCurve(mint);
const [creatorVault] = findPumpCreatorVault(sharingConfig);
const [vaultAuth] = findCoinCreatorVaultAuth(sharingConfig);Error handling
Parse program errors from failed transactions:
import { extractProgramError, ChiefStakerError } from "@karpeleslab/chiefstaker-sdk";
try {
await client.sendTransaction([ix], [keypair]);
} catch (err) {
const parsed = extractProgramError(err);
if (parsed) {
console.error(`${parsed.name}: ${parsed.message}`);
// e.g. "StakeLocked: Stake is locked - lock duration has not elapsed"
if (parsed.code === ChiefStakerError.CooldownRequired) {
// Use requestUnstake flow instead of direct unstake
}
}
}Account types
StakingPool
| Field | Type | Description |
|-----------------------------|--------------|-------------------------------------------------------|
| mint | PublicKey | Token mint address |
| tokenVault | PublicKey | PDA holding staked tokens |
| authority | PublicKey | Admin authority |
| totalStaked | bigint | Total tokens staked |
| tauSeconds | bigint | Time constant for weight curve (seconds) |
| baseTime | bigint | Base time for rebasing (Unix timestamp) |
| accRewardPerWeightedShare | bigint | Accumulated reward per weighted share (WAD-scaled) |
| lastUpdateTime | bigint | Last reward update timestamp |
| bump | number | PDA bump seed |
| lastSyncedLamports | bigint | Last known lamport balance |
| minStakeAmount | bigint | Minimum stake amount (0 = no minimum) |
| lockDurationSeconds | bigint | Lock period after staking (0 = no lock) |
| unstakeCooldownSeconds | bigint | Cooldown period for unstaking (0 = direct unstake) |
| initialBaseTime | bigint | Original base_time before rebasing |
| totalRewardDebt | bigint | Sum of all users' reward_debt (WAD-scaled) |
| totalResidualUnpaid | bigint | Lamports owed to fully-unstaked users |
UserStake
| Field | Type | Description |
|-----------------------|-------------|---------------------------------------------------|
| owner | PublicKey | Stake owner |
| pool | PublicKey | Pool this stake belongs to |
| amount | bigint | Tokens staked |
| stakeTime | bigint | When the stake began (Unix timestamp) |
| expStartFactor | bigint | Exponential factor at stake time (WAD-scaled) |
| rewardDebt | bigint | Reward debt snapshot (WAD-scaled) |
| bump | number | PDA bump seed |
| unstakeRequestAmount| bigint | Pending unstake amount (0 = none) |
| unstakeRequestTime | bigint | When unstake was requested |
| lastStakeTime | bigint | Most recent deposit timestamp |
| baseTimeSnapshot | bigint | Pool base_time when last calibrated |
| totalRewardsClaimed | bigint | Cumulative SOL claimed (lamports) |
| claimedRewardsWad | bigint | Cumulative WAD-scaled rewards paid |
| unstakeRequestSettled| number | 1 if pending request was settled at request time |
PoolMetadata
| Field | Type | Description |
|---------------|-------------|----------------------------|
| pool | PublicKey | Back-reference to pool |
| name | string | Pool display name |
| tags | string[] | Tags (e.g. #stakingpool) |
| url | string | URL |
| memberCount | bigint | Active staker count |
| bump | number | PDA bump seed |
Constants
import {
PROGRAM_ID, // ChiefStaker program
TOKEN_PROGRAM_ID, // SPL Token
TOKEN_2022_PROGRAM_ID, // Token 2022
WAD, // 10^18 (fixed-point scale)
} from "@karpeleslab/chiefstaker-sdk";How time-weighted staking works
ChiefStaker uses an exponential decay curve for staking weight:
weight = stake_amount * (1 - e^(-age / tau))- New stakers start with ~0% weight
- At 1 tau (e.g., 30 days): ~63% weight
- At 3 tau: ~95% weight
- At 5 tau: ~99% weight
This means long-term stakers earn proportionally more rewards than recent stakers, incentivizing commitment. When adding to an existing stake, the maturity percentage is preserved through blending.
License
MIT
