Skip to content

KarpelesLab/chiefstaker-sdk

Repository files navigation

@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:

  1. ChiefStakerClient — high-level class with account fetching, PDA derivation, instruction building, and transaction sending in one place.
  2. Instruction builders — standalone create*Instruction() functions that return a TransactionInstruction for composing into your own transactions.
  3. Account helpersfetch*() and deserialize*() 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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors