> For the complete documentation index, see [llms.txt](https://docs.zebec.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zebec.io/developer-docs/sdks/staking-sdk.md).

# Staking SDK

Zebec Staking SDK — integrate $ZBCN staking, reward tracking, and tier calculations on Solana.

A TypeScript SDK for interacting with the Zebec Staking protocol on Solana. This SDK provides a comprehensive interface for creating staking pools (lockups), managing stakes, tracking rewards, and querying on-chain stake data for the Zebec Network.

## Features

* **Lockup Management**: Create and update staking pools with configurable reward schemes, fees, and minimum stake requirements
* **Stake & Unstake**: Seamlessly stake tokens into lockups and unstake to claim principal + accrued rewards in a single operation
* **Reward Schemes**: Define multiple lock-period / reward-rate pairs per lockup to incentivize longer commitments
* **Human-Friendly Units**: SDK automatically converts between basis points and percentages, and between raw token amounts and UI units
* **Batch Querying**: Efficiently fetch all stakes for a user with built-in rate limiting, chunked RPC calls, and exponential backoff
* **PDA Utilities**: Helpers for deriving lockup, stake vault, reward vault, user nonce, and stake PDAs
* **Provider Abstractions**: Separate read-only and read-write provider helpers for flexible integration patterns
* **Type Safety**: Full TypeScript support with comprehensive type definitions

## Availability

{% hint style="info" %}
**Solana Only**

Staking is currently available **exclusively on Solana**. Both `mainnet-beta` and `devnet` are supported.
{% endhint %}

## Installation

```bash
npm install @zebec-network/zebec-stake-sdk
```

```bash
yarn add @zebec-network/zebec-stake-sdk
```

## Quick Start

### Setting Up the Service

```typescript
import { Connection } from "@solana/web3.js";
import {
  StakeServiceBuilder,
  createAnchorProvider,
} from "@zebec-network/zebec-stake-sdk";

// Create a connection to devnet or mainnet-beta
const connection = new Connection("https://api.devnet.solana.com");

// Create an AnchorProvider using your wallet
const provider = createAnchorProvider(connection, wallet);

// Build the service with fluent configuration
const stakeService = new StakeServiceBuilder()
  .setNetwork("devnet")
  .setProvider(provider)
  .setProgram()
  .build();
```

### Creating a Lockup (Staking Pool)

```typescript
import { deriveLockupAddress } from "@zebec-network/zebec-stake-sdk";

const lockupName = "zbcn-rewards-pool";

const tx = await stakeService.initLockup({
  name: lockupName,
  stakeToken: "ZBCNTokenMintAddressHere",
  rewardToken: "RewardTokenMintAddressHere",
  fee: 2.5,                                 // Platform fee in percent
  feeVault: "FeeVaultAddressHere",
  minimumStake: 100,                        // Minimum stake in UI token units
  rewardSchemes: [
    { duration: 2592000, rewardRate: 5 },   // 30 days → 5% reward
    { duration: 7776000, rewardRate: 15 },  // 90 days → 15% reward
    { duration: 15552000, rewardRate: 35 }, // 180 days → 35% reward
  ],
});

const signature = await tx.execute();
console.log("Lockup created:", signature);

// Derive the lockup address for later use
const [lockupAddress] = deriveLockupAddress(lockupName);
```

### Staking Tokens

```typescript
const stakeTx = await stakeService.stake({
  lockupName: "zbcn-rewards-pool",
  amount: "5000",        // Amount in human-readable token units
  lockPeriod: 7776000,   // Must match one of the lockup's configured durations
  nonce: 0n,             // User nonce (0 for the first stake)
});

const stakeSignature = await stakeTx.execute();
console.log("Staked:", stakeSignature);
```

### Unstaking (Claim Principal + Rewards)

```typescript
const unstakeTx = await stakeService.unstake({
  lockupName: "zbcn-rewards-pool",
  nonce: 0n,             // The nonce of the stake you want to unstake
});

const unstakeSignature = await unstakeTx.execute();
console.log("Unstaked:", unstakeSignature);
```

## API Reference

### `StakeServiceBuilder`

A fluent builder for constructing a `StakeService` instance with validated configuration.

#### `setNetwork(network)`

Sets the target Solana network. Defaults to `"mainnet-beta"`. Can only be called once.

```typescript
setNetwork(network?: "mainnet-beta" | "devnet"): StakeServiceBuilder
```

#### `setProvider(provider)`

Sets the provider. Must be called after `setNetwork`. If omitted, defaults to a `ReadonlyProvider` on the public cluster RPC for the chosen network. Validates that the provider's RPC endpoint matches the configured network.

```typescript
setProvider(provider?: ReadonlyProvider | AnchorProvider): StakeServiceBuilder
```

#### `setProgram(createProgram)`

Sets the Anchor program instance. Must be called after `setProvider`. If omitted, defaults to `new Program(ZEBEC_STAKE_IDL_V1, provider)`.

```typescript
setProgram(
  createProgram?: (provider: Provider) => Program<ZebecStakeIdlV1>
): StakeServiceBuilder
```

#### `build()`

Builds and returns the `StakeService`. Throws if network, provider, or program is missing, or if any builder method was called twice.

```typescript
build(): StakeService
```

***

### `StakeService`

The main service class for interacting with Zebec staking.

**Constructor properties (all `readonly`):**

| Property     | Type                         | Description                 |
| ------------ | ---------------------------- | --------------------------- |
| `provider`   | `Provider`                   | Anchor or readonly provider |
| `program`    | `Program<ZebecStakeIdlV1>`   | Anchor program instance     |
| `network`    | `"mainnet-beta" \| "devnet"` | Target network              |
| `programId`  | `PublicKey`                  | On-chain stake program ID   |
| `connection` | `Connection`                 | Solana RPC connection       |

***

### Lockup Operations (Admin)

#### `initLockup(params)`

Creates a new staking lockup pool. Derives the lockup, stake vault, and reward vault PDAs automatically from the provided `name`. Fee and reward rates are provided as human-readable percentages and converted to basis points on-chain. `minimumStake` is provided in UI units and scaled by the stake token's decimals.

**Parameters:**

| Field           | Type             | Description                                                |
| --------------- | ---------------- | ---------------------------------------------------------- |
| `name`          | `string`         | Unique name for the lockup (used to derive the PDA)        |
| `stakeToken`    | `Address`        | SPL token mint address for the token being staked          |
| `rewardToken`   | `Address`        | SPL token mint address for reward distribution             |
| `fee`           | `Numeric`        | Platform fee as a percentage (e.g., `2.5` for 2.5%)        |
| `feeVault`      | `Address`        | Address that receives protocol fees                        |
| `minimumStake`  | `Numeric`        | Minimum stake amount in UI token units                     |
| `rewardSchemes` | `RewardScheme[]` | Array of `{ duration: number; rewardRate: Numeric }` pairs |
| `creator?`      | `Address`        | Optional creator address (defaults to provider wallet)     |

**Returns:** `Promise<TransactionPayload>`

***

#### `updateLockup(params)`

Updates an existing lockup's fee, fee vault, reward schemes, and minimum stake (admin only).

**Parameters:**

| Field           | Type             | Description                                            |
| --------------- | ---------------- | ------------------------------------------------------ |
| `lockupName`    | `string`         | Name of the existing lockup                            |
| `fee`           | `Numeric`        | New platform fee percentage                            |
| `feeVault`      | `Address`        | New fee vault address                                  |
| `minimumStake`  | `Numeric`        | New minimum stake in UI units                          |
| `rewardSchemes` | `RewardScheme[]` | New reward scheme configuration                        |
| `updater?`      | `Address`        | Optional updater address (defaults to provider wallet) |

**Returns:** `Promise<TransactionPayload>`

***

### Staker Operations

#### `stake(params)`

Stakes tokens into a lockup. The SDK validates that the lockup exists and that `lockPeriod` exactly matches one of the lockup's configured durations. It fetches the current user nonce from the `UserNonce` PDA, derives the stake PDA, and scales the amount by the stake token's decimals.

**Parameters:**

| Field        | Type      | Description                                                      |
| ------------ | --------- | ---------------------------------------------------------------- |
| `lockupName` | `string`  | Name of the target lockup                                        |
| `amount`     | `Numeric` | Amount to stake in human-readable token units                    |
| `lockPeriod` | `number`  | Lock duration in seconds (must match a configured scheme)        |
| `nonce`      | `bigint`  | User nonce for this stake (typically the current on-chain nonce) |
| `staker?`    | `Address` | Optional staker address (defaults to provider wallet)            |
| `feePayer?`  | `Address` | Optional fee payer (defaults to staker)                          |

**Returns:** `Promise<TransactionPayload>`

***

#### `unstake(params)`

Unstakes a specific stake and claims the principal plus accrued rewards in a single transaction. The on-chain program transfers the staked amount and reward to the staker, and sends the platform fee to the fee vault. There is no separate "claim rewards" method — unstaking is the only way to realize rewards.

**Parameters:**

| Field        | Type      | Description                                           |
| ------------ | --------- | ----------------------------------------------------- |
| `lockupName` | `string`  | Name of the lockup                                    |
| `nonce`      | `bigint`  | Nonce of the stake to unstake                         |
| `staker?`    | `Address` | Optional staker address (defaults to provider wallet) |
| `feePayer?`  | `Address` | Optional fee payer (defaults to staker)               |

**Returns:** `Promise<TransactionPayload>`

***

### Information Retrieval

#### `getLockupInfo(lockupAddress)`

Retrieves detailed information about a lockup. Returns `null` if the lockup does not exist. Fee and reward rates are converted from basis points back to percentages; amounts are returned in UI units.

**Returns:** `Promise<LockupInfo \| null>`

```typescript
const info = await stakeService.getLockupInfo(lockupAddress);
console.log(info.stakeInfo.name);
console.log(info.feeInfo.fee);              // e.g., "2.5" (percent)
console.log(info.stakeInfo.minimumStake);   // UI amount
console.log(info.stakeInfo.rewardSchemes);  // [{ duration, rewardRate }]
```

***

#### `getStakeInfo(stakeAddress, lockupAddress)`

Retrieves information about a specific stake. Throws if the lockup does not exist; returns `null` if the stake account is missing.

**Returns:** `Promise<StakeInfo \| null>`

```typescript
const stake = await stakeService.getStakeInfo(stakeAddress, lockupAddress);
console.log(stake.stakedAmount);   // UI amount
console.log(stake.rewardAmount);   // UI amount
console.log(stake.lockPeriod);     // seconds
console.log(stake.stakeClaimed);   // boolean
```

***

#### `getUserNonceInfo(userNonceAddress)`

Retrieves the nonce counter for a user within a specific lockup. Returns `null` if the account does not exist.

**Returns:** `Promise<UserNonceInfo \| null>`

```typescript
const nonceInfo = await stakeService.getUserNonceInfo(userNonceAddress);
console.log(nonceInfo.nonce); // bigint
```

***

#### `getAllStakesInfoOfUser(userAddress, lockupAddress, options?)`

Fetches all stakes for a user within a lockup. Derives all stake PDAs from nonce `0` through the current nonce, fetches them in chunks of 100 via `getMultipleAccountsInfo`, and resolves each stake's transaction hash through a rate-limited queue. Returns `[]` if the user has no nonce account.

**Options:**

| Field           | Type     | Default | Description                        |
| --------------- | -------- | ------- | ---------------------------------- |
| `minDelayMs`    | `number` | `400`   | Minimum delay between RPC requests |
| `maxConcurrent` | `number` | `3`     | Max concurrent signature lookups   |

**Returns:** `Promise<StakeInfoWithHash[]>`

***

#### `getAllStakesInfo(lockupAddress)`

Fetches all stake accounts for a given lockup using `getProgramAccounts`.

**Returns:** `Promise<StakeInfo[]>`

***

#### `getAllStakesCount(lockupAddress)`

Returns the total number of stakes in a lockup (efficient count-only query).

**Returns:** `Promise<number>`

***

#### `getStakeSignatureForStake(stakeInfo)`

Finds the original staking transaction signature by matching the stake's `createdTime` against the account's signature history. Uses exponential backoff for RPC resilience.

**Returns:** `Promise<string \| null>`

***

### Low-Level Instruction Builders

Each high-level method has a corresponding instruction builder for composing custom transactions:

| Method                            | Returns                           |
| --------------------------------- | --------------------------------- |
| `getInitLockupInstruction(...)`   | `Promise<TransactionInstruction>` |
| `getUpdateLockupInstruction(...)` | `Promise<TransactionInstruction>` |
| `getStakeInstruction(...)`        | `Promise<TransactionInstruction>` |
| `getUnstakeInstruction(...)`      | `Promise<TransactionInstruction>` |

***

## Provider Setup

### AnchorProvider (Read/Write)

Use for operations that sign transactions:

```typescript
import { createAnchorProvider } from "@zebec-network/zebec-stake-sdk";

const provider = createAnchorProvider(connection, wallet, {
  commitment: "confirmed",
  preflightCommitment: "confirmed",
});
```

### ReadonlyProvider (Read-Only)

Use for read-only queries without a wallet:

```typescript
import { createReadonlyProvider } from "@zebec-network/zebec-stake-sdk";

const provider = createReadonlyProvider(connection, optionalWalletAddress);
```

The `ReadonlyProvider` class exposes `connection` and an optional `walletAddress` for convenience in read-only contexts.

### Wallet Interface

The SDK exports an `AnchorWallet` interface for wallets that work with `createAnchorProvider`:

```typescript
import type { AnchorWallet } from "@zebec-network/zebec-stake-sdk";
```

***

## PDA Utilities

```typescript
import {
  deriveLockupAddress,
  deriveStakeAddress,
  deriveUserNonceAddress,
  deriveStakeVaultAddress,
  deriveRewardVaultAddress,
} from "@zebec-network/zebec-stake-sdk";

// Derive the lockup PDA from its name
const [lockupPda] = deriveLockupAddress("my-lockup");

// Derive a user's nonce PDA
const [userNoncePda] = deriveUserNonceAddress(userPublicKey, lockupPda);

// Derive a specific stake PDA
const [stakePda] = deriveStakeAddress(userPublicKey, lockupPda, 0n);

// Derive vault PDAs
const [stakeVaultPda] = deriveStakeVaultAddress(lockupPda);
const [rewardVaultPda] = deriveRewardVaultAddress(lockupPda);
```

***

## Types

### `LockupInfo`

```typescript
type LockupInfo = {
  address: string;
  feeInfo: {
    fee: string;       // Platform fee as a percentage string
    feeVault: string;
  };
  rewardToken: {
    tokenAddress: string;
  };
  stakeToken: {
    tokenAdress: string;  // Stake token mint
    totalStaked: string;  // Total staked in UI units
  };
  stakeInfo: {
    name: string;
    creator: string;
    rewardSchemes: RewardScheme[];
    minimumStake: string; // Minimum stake in UI units
  };
};
```

### `StakeInfo`

```typescript
type StakeInfo = {
  address: string;
  nonce: bigint;
  createdTime: number;     // Unix timestamp
  stakedAmount: string;    // UI units
  rewardAmount: string;    // UI units
  stakeClaimed: boolean;
  lockPeriod: number;      // seconds
  staker: string;
  lockup: string;
};
```

### `StakeInfoWithHash`

```typescript
type StakeInfoWithHash = StakeInfo & {
  hash: string; // Original staking transaction signature
};
```

### `UserNonceInfo`

```typescript
type UserNonceInfo = {
  address: string;
  nonce: bigint;
};
```

### `RewardScheme`

```typescript
type RewardScheme = {
  duration: number;    // Lock duration in seconds
  rewardRate: Numeric; // Reward rate as a percentage
};
```

### `Numeric`

```typescript
type Numeric = string | number;
```

***

## Usage Examples

### Complete Stake Lifecycle

```typescript
import { Connection } from "@solana/web3.js";
import {
  StakeServiceBuilder,
  createAnchorProvider,
  deriveLockupAddress,
  deriveUserNonceAddress,
  deriveStakeAddress,
} from "@zebec-network/zebec-stake-sdk";

const connection = new Connection("https://api.devnet.solana.com");
const provider = createAnchorProvider(connection, wallet);
const stakeService = new StakeServiceBuilder()
  .setNetwork("devnet")
  .setProvider(provider)
  .setProgram()
  .build();

const lockupName = "zbcn-rewards-pool";
const [lockupAddress] = deriveLockupAddress(lockupName);

// 1. Create a lockup (admin operation)
const initTx = await stakeService.initLockup({
  name: lockupName,
  stakeToken: zbcnMint,
  rewardToken: rewardMint,
  fee: 2.5,
  feeVault: feeVaultAddress,
  minimumStake: 100,
  rewardSchemes: [
    { duration: 2592000, rewardRate: 5 },
    { duration: 7776000, rewardRate: 15 },
    { duration: 15552000, rewardRate: 35 },
  ],
});
await initTx.execute();

// 2. Stake tokens for 90 days
const stakeTx = await stakeService.stake({
  lockupName,
  amount: "5000",
  lockPeriod: 7776000,
  nonce: 0n,
});
await stakeTx.execute();

// 3. Fetch user nonce and stake info
const [userNoncePda] = deriveUserNonceAddress(wallet.publicKey, lockupAddress);
const nonceInfo = await stakeService.getUserNonceInfo(userNoncePda);

const [stakePda] = deriveStakeAddress(
  wallet.publicKey,
  lockupAddress,
  nonceInfo!.nonce - 1n
);
const stakeInfo = await stakeService.getStakeInfo(stakePda, lockupAddress);
console.log("Staked:", stakeInfo?.stakedAmount);
console.log("Pending reward:", stakeInfo?.rewardAmount);

// 4. Unstake to claim principal + rewards
const unstakeTx = await stakeService.unstake({
  lockupName,
  nonce: 0n,
});
await unstakeTx.execute();
```

### Fetch All Stakes for a User

```typescript
const allStakes = await stakeService.getAllStakesInfoOfUser(
  wallet.publicKey.toBase58(),
  lockupAddress,
  { minDelayMs: 400, maxConcurrent: 3 }
);

allStakes.forEach((stake) => {
  console.log("Stake:", stake.stakedAmount, "for", stake.lockPeriod, "seconds");
  console.log("Reward:", stake.rewardAmount);
  console.log("Tx hash:", stake.hash);
});
```

### Query Lockup Statistics

```typescript
const lockup = await stakeService.getLockupInfo(lockupAddress);
console.log("Pool:", lockup?.stakeInfo.name);
console.log("Total staked:", lockup?.stakeToken.totalStaked);
console.log("Fee:", lockup?.feeInfo.fee, "%");
console.log("Schemes:", lockup?.stakeInfo.rewardSchemes);

const totalStakes = await stakeService.getAllStakesCount(lockupAddress);
console.log("Active stakes:", totalStakes);
```

### Error Handling

```typescript
import { deriveLockupAddress } from "@zebec-network/zebec-stake-sdk";

try {
  const tx = await stakeService.stake({
    lockupName: "zbcn-rewards-pool",
    amount: "5000",
    lockPeriod: 7776000,
    nonce: 0n,
  });
  const signature = await tx.execute();
  console.log("Success:", signature);
} catch (error) {
  if (error.message.includes("Invalid lockperiod")) {
    console.error("lockPeriod must match one of the lockup's configured durations");
  } else if (error.message.includes("Lockup account does not exists")) {
    console.error("The specified lockup does not exist. Check the lockup name.");
  } else if (error.message.includes("MinimumStakeNotMet")) {
    console.error("Stake amount is below the lockup's minimum stake requirement.");
  } else {
    console.error("Staking failed:", error);
  }
}
```

***

## Constants

| Constant                                  | Description                                                           |
| ----------------------------------------- | --------------------------------------------------------------------- |
| `ZEBEC_STAKE_PROGRAM.mainnet`             | Program ID (`zSTKzGLiN6T6EVzhBiL6sjULXMahDavAS2p4R62afGv`)            |
| `ZEBEC_STAKE_PROGRAM.devnet`              | Program ID (same as mainnet)                                          |
| `STAKE_LOOKUP_TABLE_ADDRESS.mainnet-beta` | Address lookup table (`EoKjJejKr4XsBdtUuYwzZcYd6tpGNijxCGgQocxtxQ8t`) |
| `STAKE_LOOKUP_TABLE_ADDRESS.devnet`       | Address lookup table (`C4R2sL6yj7bzKfbdfwCfH68DZZ3QnzdmedE9wQqTfAAA`) |
| `SEEDS.lockup`                            | PDA seed for lockup (`zebec_lockup`)                                  |
| `SEEDS.stakeVault`                        | PDA seed for stake vault (`stake_vault`)                              |
| `SEEDS.rewardVault`                       | PDA seed for reward vault (`reward_vault`)                            |

***

## Dependencies

| Package                        | Purpose                                                          |
| ------------------------------ | ---------------------------------------------------------------- |
| `@coral-xyz/anchor`            | Anchor framework for Solana program interaction                  |
| `@solana/web3.js`              | Solana Web3 JavaScript API                                       |
| `@zebec-network/core-utils`    | Zebec core utilities (BPS conversions, sleep)                    |
| `@zebec-network/solana-common` | Common Solana helpers (ATAs, transaction payload, mint decimals) |
| `bignumber.js`                 | Arbitrary-precision arithmetic for token amounts                 |
| `bn.js`                        | BigNumber utilities for Anchor compatibility                     |

***

## On-Chain Error Reference

The SDK maps on-chain error codes to human-readable messages. Common errors you may encounter:

| Code | Name                      | Meaning                                            |
| ---- | ------------------------- | -------------------------------------------------- |
| 6000 | `InvalidTime`             | Invalid timestamp or time parameter                |
| 6001 | `InvalidStakeToken`       | The provided stake token is not allowed            |
| 6002 | `InvalidRewardToken`      | The provided reward token is not allowed           |
| 6003 | `InvalidStakePeriod`      | The lock period does not match a configured scheme |
| 6004 | `InvalidStaker`           | The staker is not authorized                       |
| 6005 | `InvaildNonce`            | Invalid nonce provided                             |
| 6006 | `UnAuthorized`            | Caller lacks required permissions                  |
| 6007 | `InvalidLockPeriod`       | Lock period is invalid                             |
| 6008 | `InvalidAmount`           | Stake or reward amount is invalid                  |
| 6009 | `RewardAlreadyClaimed`    | Rewards have already been claimed for this stake   |
| 6010 | `StakeRewardNotClaimable` | Rewards are not yet claimable                      |
| 6011 | `RewardIsZero`            | Calculated reward is zero                          |
| 6012 | `StakeAlreadyClaimed`     | Stake has already been claimed/unstaked            |
| 6013 | `StakeNotClaimable`       | Stake is not yet eligible for unstaking            |
| 6014 | `MinimumStakeNotMet`      | Stake amount is below the minimum requirement      |

***

## Related

* [Zebec tokenomics blog](https://zebec.io/blog/zebec-network-zbcn-tokenomics)
* [Web SuperApp yield products](/zebec-product-information/yield.md)
* [Solana Streaming SDK](/developer-docs/sdks/streaming-sdk/solana-streaming-sdk.md)
