@zebec-network/zebec-stake-sdk
v1.3.1
Published
An SDK for zebec network stake solana program
Downloads
203
Readme
Zebec Stake Sdk
An SDK for interacting with the Zebec Network staking program on Solana. Supports creating and managing staking lockup pools, staking/unstaking tokens, and querying on-chain staking data.
Table of Contents
Installation
npm install @zebec-network/zebec-stake-sdkyarn add @zebec-network/zebec-stake-sdkpnpm add @zebec-network/zebec-stake-sdkQuick Setup
Read-only (no wallet required)
Use this setup for querying on-chain data without signing transactions.
import { Connection } from "@solana/web3.js";
import { createReadonlyProvider, StakeServiceBuilder } from "@zebec-network/zebec-stake-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const provider = createReadonlyProvider(connection);
const service = new StakeServiceBuilder()
.setNetwork("mainnet-beta")
.setProvider(provider)
.setProgram()
.build();With wallet (for signing transactions)
Use this setup when you need to send transactions (stake, unstake, create/update lockups).
import { Connection, Keypair } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";
import {
createAnchorProvider,
StakeServiceBuilder,
} from "@zebec-network/zebec-stake-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const keypair = Keypair.fromSecretKey(/* your secret key bytes */);
const wallet = new Wallet(keypair);
const provider = createAnchorProvider(connection, wallet, { commitment: "confirmed" });
const service = new StakeServiceBuilder()
.setNetwork("mainnet-beta")
.setProvider(provider)
.setProgram()
.build();Default setup (mainnet, no wallet)
All builder methods are optional — calling them without arguments uses sensible defaults.
import { StakeServiceBuilder } from "@zebec-network/zebec-stake-sdk";
// Defaults: mainnet-beta network, ReadonlyProvider with public RPC
const service = new StakeServiceBuilder()
.setNetwork()
.setProvider()
.setProgram()
.build();API Documentation
StakeServiceBuilder
A fluent builder for constructing a StakeService. Methods must be called in order: setNetwork → setProvider → setProgram → build.
class StakeServiceBuilder {
setNetwork(network?: "mainnet-beta" | "devnet"): StakeServiceBuilder
setProvider(provider?: ReadonlyProvider | AnchorProvider): StakeServiceBuilder
setProgram(createProgram?: (provider) => Program<ZebecStakeIdlV1>): StakeServiceBuilder
build(): StakeService
}| Method | Description |
| ------ | ----------- |
| setNetwork(network?) | Set the target network. Defaults to "mainnet-beta". Must be called before setProvider. |
| setProvider(provider?) | Set the provider. Accepts ReadonlyProvider or AnchorProvider. Defaults to ReadonlyProvider using the public cluster RPC. Must be called before setProgram. |
| setProgram(createProgram?) | Set the Anchor program. Optionally accepts a factory (provider) => Program. Defaults to the built-in IDL. |
| build() | Validates all settings and returns a StakeService instance. Throws if any step was skipped. |
Errors thrown by the builder:
"InvalidOperation: Network is set twice."—setNetworkcalled more than once."InvalidOperation: Provider is set twice."—setProvidercalled more than once."InvalidOperation: Program is set twice."—setProgramcalled more than once."InvalidOperation: Network is not set."—setProvider/setProgramcalled beforesetNetwork."InvalidOperation: Provider is not set."—setProgramcalled beforesetProvider.- Network mismatch error if the provider's RPC endpoint does not match the set network.
StakeService
The main service class. Exposes methods for managing lockup pools, staking, and reading on-chain data. All transaction methods return a TransactionPayload that must be executed separately.
initLockup(params)
Creates a new staking lockup pool. Only the creator can later update it.
service.initLockup(params: {
stakeToken: Address; // Mint address of the token to be staked
rewardToken: Address; // Mint address of the reward token
name: string; // Unique name for the lockup (used to derive its address)
fee: Numeric; // Fee amount per stake (in token units, e.g. 5 = 5 tokens)
feeVault: Address; // Public key of the account that collects fees
rewardSchemes: RewardScheme[]; // Array of lock durations and annual reward rates
minimumStake: Numeric; // Minimum stake amount (in token units)
creator?: Address; // Defaults to provider.publicKey
}): Promise<TransactionPayload>updateLockup(params)
Updates an existing lockup pool. Only callable by the original creator.
service.updateLockup(params: {
lockupName: string; // Name of the lockup to update
fee: Numeric; // New fee amount
feeVault: Address; // New fee vault address
rewardSchemes: RewardScheme[]; // Updated reward schemes
minimumStake: Numeric; // Updated minimum stake amount
updater?: Address; // Defaults to provider.publicKey
}): Promise<TransactionPayload>stake(params)
Stakes tokens into a lockup pool for a specified lock period.
service.stake(params: {
lockupName: string; // Name of the lockup to stake into
amount: Numeric; // Amount to stake (in token units, e.g. 100 = 100 tokens)
lockPeriod: number; // Lock duration in seconds — must match an existing reward scheme
nonce: bigint; // Current user nonce (use getUserNonceInfo to retrieve)
feePayer?: Address; // Defaults to staker
staker?: Address; // Defaults to provider.publicKey
}): Promise<TransactionPayload>unstake(params)
Unstakes tokens and claims the accrued reward after the lock period has elapsed.
service.unstake(params: {
lockupName: string; // Name of the lockup
nonce: bigint; // Nonce of the stake to unstake
feePayer?: Address; // Defaults to staker
staker?: Address; // Defaults to provider.publicKey
}): Promise<TransactionPayload>getLockupInfo(lockupAddress)
Fetches and returns human-readable information about a lockup pool.
service.getLockupInfo(lockupAddress: Address): Promise<LockupInfo | null>Returns null if the lockup does not exist.
getStakeInfo(stakeAddress, lockupAddress)
Fetches information about a specific stake position.
service.getStakeInfo(stakeAddress: Address, lockupAddress: Address): Promise<StakeInfo | null>Returns null if the stake account does not exist. Throws if the lockup does not exist.
getUserNonceInfo(userNonceAddress)
Returns the user's current nonce for a given lockup. The nonce is used to derive stake addresses and must be passed when calling stake().
service.getUserNonceInfo(userNonceAddress: Address): Promise<UserNonceInfo | null>Returns null if the user has never staked in this lockup (nonce is implicitly 0n).
getAllStakesInfoOfUser(userAddress, lockupAddress, options?)
Fetches all historical stake positions for a user in a given lockup pool, including the transaction signature for each stake.
service.getAllStakesInfoOfUser(
userAddress: Address,
lockupAddress: Address,
options?: {
minDelayMs?: number; // Minimum ms between RPC calls (default: 400)
maxConcurrent?: number; // Max concurrent RPC calls (default: 3)
}
): Promise<StakeInfoWithHash[]>getAllStakesInfo(lockupAddress)
Fetches all stake positions across all users in a lockup pool.
service.getAllStakesInfo(lockupAddress: Address): Promise<StakeInfo[]>getAllStakesCount(lockupAddress)
Returns the total number of stake accounts associated with a lockup pool.
service.getAllStakesCount(lockupAddress: Address): Promise<number>getStakeSignatureForStake(stakeInfo)
Retrieves the on-chain transaction signature for a given stake position.
service.getStakeSignatureForStake(stakeInfo: StakeInfo): Promise<string | null>Properties
| Property | Type | Description |
| -------- | ---- | ----------- |
| programId | PublicKey | The deployed staking program's public key |
| connection | Connection | The active Solana RPC connection |
| provider | Provider | The underlying Anchor/Readonly provider |
| program | Program<ZebecStakeIdlV1> | The Anchor program instance |
Providers
Two provider types are available depending on your use case.
createReadonlyProvider(connection, walletAddress?)
Creates a lightweight provider for read-only operations. Does not require a wallet.
import { createReadonlyProvider } from "@zebec-network/zebec-stake-sdk";
const provider = createReadonlyProvider(connection);
// or with an optional wallet address for context
const provider = createReadonlyProvider(connection, "YourWalletPublicKey...");createAnchorProvider(connection, wallet, options?)
Creates a full Anchor provider capable of signing and sending transactions.
import { createAnchorProvider } from "@zebec-network/zebec-stake-sdk";
const provider = createAnchorProvider(connection, wallet, {
commitment: "confirmed",
});The wallet must implement the AnchorWallet interface:
interface AnchorWallet {
publicKey: PublicKey;
signTransaction<T extends Transaction | VersionedTransaction>(tx: T): Promise<T>;
signAllTransactions<T extends Transaction | VersionedTransaction>(txs: T[]): Promise<T[]>;
}PDA Utilities
Helper functions for deriving program-derived addresses. All programId parameters default to the mainnet program ID.
import {
deriveLockupAddress,
deriveStakeAddress,
deriveUserNonceAddress,
deriveStakeVaultAddress,
deriveRewardVaultAddress,
} from "@zebec-network/zebec-stake-sdk";| Function | Description |
| -------- | ----------- |
| deriveLockupAddress(name, programId?) | Derives the lockup PDA from its unique name |
| deriveStakeAddress(staker, lockup, nonce, programId?) | Derives a stake PDA for a given staker, lockup, and nonce |
| deriveUserNonceAddress(user, lockup, programId?) | Derives the user nonce PDA tracking a user's total stake count |
| deriveStakeVaultAddress(lockup, programId?) | Derives the vault PDA that holds staked tokens |
| deriveRewardVaultAddress(lockup, programId?) | Derives the vault PDA that holds reward tokens |
Types
// Human-readable reward scheme: duration in seconds and annual rate as a percentage
type RewardScheme = {
duration: number; // Lock period in seconds (e.g., 2592000 = 30 days)
rewardRate: Numeric; // Annual reward rate as a percentage (e.g., "5.00" = 5%)
};
// Returned by getLockupInfo()
type LockupInfo = {
address: string;
feeInfo: {
fee: string; // Fee amount in token units
feeVault: string; // Fee collector address
};
rewardToken: {
tokenAddress: string;
};
stakeToken: {
tokenAdress: string; // Note: single 'd' in 'adress' (matches on-chain field)
totalStaked: string; // Total tokens currently staked in this lockup
};
stakeInfo: {
name: string;
creator: string;
rewardSchemes: RewardScheme[];
minimumStake: string;
};
};
// Returned by getStakeInfo() and getAllStakesInfo()
type StakeInfo = {
address: string; // Stake PDA address
nonce: bigint; // Stake nonce (index within this user's stakes)
createdTime: number; // Unix timestamp of when the stake was created
stakedAmount: string; // Amount staked in token units
rewardAmount: string; // Accrued reward in reward token units
stakeClaimed: boolean; // Whether the stake has been unstaked
lockPeriod: number; // Lock duration in seconds
staker: string; // Staker's public key
lockup: string; // Lockup PDA address
};
// Returned by getAllStakesInfoOfUser()
type StakeInfoWithHash = StakeInfo & {
hash: string; // Transaction signature of the original stake
};
// Returned by getUserNonceInfo()
type UserNonceInfo = {
address: string; // User nonce PDA address
nonce: bigint; // Current nonce (equals total number of stakes made)
};
// Accepted wherever amounts are passed
type Numeric = string | number;Constants
import { ZEBEC_STAKE_PROGRAM, STAKE_LOOKUP_TABLE_ADDRESS } from "@zebec-network/zebec-stake-sdk";
// Program IDs
ZEBEC_STAKE_PROGRAM.mainnet // "zSTKzGLiN6T6EVzhBiL6sjULXMahDavAS2p4R62afGv"
ZEBEC_STAKE_PROGRAM.devnet // "zSTKzGLiN6T6EVzhBiL6sjULXMahDavAS2p4R62afGv"
// Address Lookup Table accounts for versioned transactions
STAKE_LOOKUP_TABLE_ADDRESS["mainnet-beta"] // "EoKjJejKr4XsBdtUuYwzZcYd6tpGNijxCGgQocxtxQ8t"
STAKE_LOOKUP_TABLE_ADDRESS["devnet"] // "C4R2sL6yj7bzKfbdfwCfH68DZZ3QnzdmedE9wQqTfAAA"Usage Examples
Read-only queries
import { Connection } from "@solana/web3.js";
import {
createReadonlyProvider,
deriveLockupAddress,
deriveUserNonceAddress,
StakeServiceBuilder,
} from "@zebec-network/zebec-stake-sdk";
const connection = new Connection("https://api.mainnet-beta.solana.com", "confirmed");
const provider = createReadonlyProvider(connection);
const service = new StakeServiceBuilder()
.setNetwork("mainnet-beta")
.setProvider(provider)
.setProgram()
.build();
// Derive the lockup address from its name
const lockupName = "ZBCN_Lockup_003";
const lockupAddress = deriveLockupAddress(lockupName, service.programId);
// Fetch lockup pool info
const lockupInfo = await service.getLockupInfo(lockupAddress);
console.log(lockupInfo);
// {
// address: "...",
// feeInfo: { fee: "5", feeVault: "..." },
// rewardToken: { tokenAddress: "..." },
// stakeToken: { tokenAdress: "...", totalStaked: "1234567" },
// stakeInfo: {
// name: "ZBCN_Lockup_003",
// creator: "...",
// rewardSchemes: [
// { duration: 2592000, rewardRate: "3.00" },
// { duration: 7776000, rewardRate: "5.00" },
// ],
// minimumStake: "1"
// }
// }
// Fetch all stakes in a lockup
const allStakes = await service.getAllStakesInfo(lockupAddress);
console.log(`Total active positions: ${allStakes.length}`);
// Fetch total stake count
const count = await service.getAllStakesCount(lockupAddress);
console.log(`Total stake accounts: ${count}`);Initialize a lockup pool
Requires an AnchorProvider (wallet with signing capability).
import { Connection, Keypair } from "@solana/web3.js";
import { Wallet } from "@coral-xyz/anchor";
import {
createAnchorProvider,
deriveLockupAddress,
StakeServiceBuilder,
} from "@zebec-network/zebec-stake-sdk";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const keypair = Keypair.fromSecretKey(/* your secret key bytes */);
const wallet = new Wallet(keypair);
const provider = createAnchorProvider(connection, wallet, { commitment: "confirmed" });
const service = new StakeServiceBuilder()
.setNetwork("devnet")
.setProvider(provider)
.setProgram()
.build();
const ZBCN_MINT = "ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU";
const payload = await service.initLockup({
stakeToken: ZBCN_MINT,
rewardToken: ZBCN_MINT,
name: "My_Lockup_001",
fee: 0, // 0 token fee per stake
feeVault: "FeeVaultPublicKey...",
minimumStake: 1, // Minimum 1 token to stake
rewardSchemes: [
{ duration: 2592000, rewardRate: "3.00" }, // 30 days @ 3% APR
{ duration: 7776000, rewardRate: "5.00" }, // 90 days @ 5% APR
{ duration: 15552000, rewardRate: "7.00" }, // 180 days @ 7% APR
],
});
const signature = await payload.execute({ commitment: "confirmed" });
console.log("Lockup created:", signature);
const lockupAddress = deriveLockupAddress("My_Lockup_001", service.programId);
const lockupInfo = await service.getLockupInfo(lockupAddress);
console.log(lockupInfo);Update a lockup pool
Only the original creator can update a lockup.
const payload = await service.updateLockup({
lockupName: "My_Lockup_001",
fee: 5,
feeVault: "NewFeeVaultPublicKey...",
minimumStake: 10,
rewardSchemes: [
{ duration: 2592000, rewardRate: "5.00" }, // 30 days @ 5% APR
{ duration: 7776000, rewardRate: "8.00" }, // 90 days @ 8% APR
{ duration: 15552000, rewardRate: "12.00" }, // 180 days @ 12% APR
],
});
const signature = await payload.execute({ commitment: "confirmed" });
console.log("Lockup updated:", signature);Stake tokens
import {
deriveUserNonceAddress,
deriveLockupAddress,
} from "@zebec-network/zebec-stake-sdk";
const lockupName = "My_Lockup_001";
const lockupAddress = deriveLockupAddress(lockupName, service.programId);
// Get the user's current nonce (determines the next stake account address)
const userNonceAddress = deriveUserNonceAddress(
wallet.publicKey,
lockupAddress,
service.programId,
);
const nonceInfo = await service.getUserNonceInfo(userNonceAddress);
const nonce = nonceInfo ? nonceInfo.nonce : 0n;
// Stake 1000 tokens for 30 days (2592000 seconds)
const payload = await service.stake({
lockupName,
amount: 1000,
lockPeriod: 2592000, // must match a duration in the lockup's rewardSchemes
nonce, // current nonce; incremented on-chain after staking
});
const signature = await payload.execute({ commitment: "confirmed" });
console.log("Stake signature:", signature);Unstake tokens
Unstaking returns the original staked tokens plus any accrued reward. Can only be called after the lock period has elapsed.
import { deriveStakeAddress } from "@zebec-network/zebec-stake-sdk";
// The nonce used when staking identifies which stake position to unstake
const stakeNonce = 0n;
const payload = await service.unstake({
lockupName: "My_Lockup_001",
nonce: stakeNonce,
});
const signature = await payload.execute({ commitment: "confirmed" });
console.log("Unstake signature:", signature);
// Verify the stake was claimed
const lockupAddress = deriveLockupAddress("My_Lockup_001", service.programId);
const stakeAddress = deriveStakeAddress(
wallet.publicKey,
lockupAddress,
stakeNonce,
service.programId,
);
const stakeInfo = await service.getStakeInfo(stakeAddress, lockupAddress);
console.log("Stake claimed:", stakeInfo?.stakeClaimed); // true
console.log("Reward received:", stakeInfo?.rewardAmount);Fetch stake data
// All stakes for a specific user in a lockup
const userStakes = await service.getAllStakesInfoOfUser(
wallet.publicKey,
lockupAddress,
{
maxConcurrent: 3, // max parallel RPC calls (default: 3)
minDelayMs: 400, // ms between requests to avoid rate limits (default: 400)
}
);
for (const stake of userStakes) {
console.log({
nonce: stake.nonce.toString(),
amount: stake.stakedAmount,
reward: stake.rewardAmount,
claimed: stake.stakeClaimed,
lockPeriod: stake.lockPeriod,
txHash: stake.hash,
});
}
// Single stake by address
const stakeAddress = deriveStakeAddress(
wallet.publicKey,
lockupAddress,
0n,
service.programId,
);
const stakeInfo = await service.getStakeInfo(stakeAddress, lockupAddress);Development
Environment setup
Create a .env file at the project root for running tests:
RPC_URL=https://your-mainnet-rpc-url
DEVNET_RPC_URL=https://your-devnet-rpc-url
# JSON arrays of base58-encoded secret keys
MAINNET_SECRET_KEYS=["base58SecretKey1","base58SecretKey2"]
DEVNET_SECRET_KEYS=["base58SecretKey1","base58SecretKey2"]Commands
# Build the package
npm run build
# Run all tests
npm test
# Run a single test file
npm run test:single ./test/e2e/getLockupInfo.test.ts
# Run a specific test by name pattern
npm run test:single ./test/e2e/stakeAndUnstake.test.ts -- -f "stake()"
# Format code
npm run formatPublish
Build and bump the version in package.json, then:
npm publish --access public