@zebec-network/zebec-vault-sdk
v5.2.4
Published
An SDK for zebec vault solana program
Readme
Introduction
A Typescript SDK for interacting with Zebec Vault on Solana, enabling secure multi operations, token streaming, staking, and virtual card management.
Features
- Vault Management: Create and manage secure vaults with proposal-based execution
- Token Operations: Deposit and withdraw SOL and SPL tokens
- Streaming Payments: Create, manage, and cancel payment streams
- Staking: Stake tokens with customizable lock periods
- Virtual Cards: Create and load Zebec cards with automatic token swapping
- Jupiter Integration: Built-in token swapping for card operations
Installation
npm install @zebec-network/zebec-vault-sdkyarn add @zebec-network/zebec-vault-sdkQuick Start
Setup
import { Connection, Keypair } from "@solana/web3.js";
import { createAnchorProvider, ZebecVaultService } from "@zebec-network/zebec-vault-sdk";
// Create connection
const connection = new Connection("https://api.mainnet-beta.solana.com"); // use a private dedicated RPC for production
// Create wallet adapter
// For a frontend application, use the wallet provided by the wallet adapter:
const wallet = useAnchorWallet();
// For a server-side app, use the Wallet class from @coral-xyz/anchor:
const wallet = new Wallet(keypair); // create keypair from secret key
// Create provider
const provider = createAnchorProvider(connection, wallet);
// Initialize service
const vaultService = ZebecVaultService.create(provider, "mainnet-beta");Core Features
Vault Operations
Each user has a vault that is derived from a seed containing the user's wallet. A vault signer is then derived from the vault as a seed. The vault signer holds SPL token and SOL balances and signs transactions during CPI executions.
Create a vault
const payload = await vaultService.createVault({
payer: wallet.publicKey, // optional if using AnchorProvider
});
const signature = await payload.execute();
console.log("Vault created:", signature);Get Vault Information
// Get a specific user's vault (param is optional when using AnchorProvider)
const vaultInfo = await vaultService.getVaultInfoOfUser(userAddress);
if (vaultInfo) {
console.log("Vault:", vaultInfo.vault.toString());
console.log("Owner:", vaultInfo.owner.toString());
console.log("Created:", new Date(vaultInfo.createdDate * 1000));
}
// Get all vaults
const allVaults = await vaultService.getAllVaultsInfo();
console.log(`Total vaults: ${allVaults.length}`);Deposit SOL
const payload = await vaultService.depositSol({
depositor: wallet.publicKey,
amount: 1.5, // SOL amount
});
const signature = await payload.execute();Deposit SPL Tokens
const payload = await vaultService.deposit({
depositor: wallet.publicKey,
tokenMint: "TOKEN_MINT_ADDRESS",
amount: 100, // token amount
});
const signature = await payload.execute();Note: If the WSOL address is given as
tokenMint, SOL will be wrapped to WSOL and deposited as an SPL token.
Withdraw SOL
const payload = await vaultService.withdrawSol({
withdrawer: wallet.publicKey,
amount: 0.5,
});
const signature = await payload.execute();Withdraw SPL Tokens
const payload = await vaultService.withdraw({
withdrawer: wallet.publicKey,
tokenMint: "TOKEN_MINT_ADDRESS",
amount: 50,
});
const signature = await payload.execute();Note: If the WSOL address is given as
tokenMint, WSOL will be withdrawn and unwrapped into the withdrawer's wallet.
Proposal System
The Proposal System includes methods for creating proposals, appending actions, and executing proposals. A proposal is like a Solana transaction but with extra fields such as proposal stage, created date, expiry date, execution status, and name. Each proposal contains one or more ProposalAction, which is similar to a TransactionInstruction in Solana. During execution, proposal actions run in order. You can delete a proposal to reclaim the SOL rent used to create it. Each proposal belongs to a vault and only the vault signer can execute, delete, or append actions to it.
Create a Proposal
import { SystemProgram } from "@solana/web3.js";
import { parseSol } from "@zebec-network/solana-common";
import { deriveUserVault, deriveVaultSigner } from "@zebec-network/zebec-vault-sdk";
const [vault] = deriveUserVault(wallet.publicKey, vaultService.vaultV1ProgramId);
const [vaultSigner] = deriveVaultSigner(vault, vaultService.vaultV1ProgramId);
const recipient = "<Recipient PublicKey>";
// Create custom instructions. For example:
const instruction = SystemProgram.transfer({
fromPubkey: vaultSigner,
toPubkey: recipient,
lamports: Number(parseSol(1)),
});
const payload = await vaultService.createProposal({
proposer: wallet.publicKey,
name: "Transfer SOL",
actions: [instruction],
// proposalKeypair: Keypair.generate(), // optional; auto-generated if omitted
});
const signature = await payload.execute();Get Proposals
const proposals = await vaultService.getProposalsInfoOfVault(vaultAddress);
proposals.forEach((proposal) => {
console.log("Proposal:", proposal.name);
console.log("Status:", proposal.proposalStage);
console.log("Actions:", proposal.actions.length);
console.log("Executed:", proposal.isExecuted);
});Append Actions
import { SystemProgram } from "@solana/web3.js";
import { parseSol } from "@zebec-network/solana-common";
const proposal = "<Proposal PublicKey>";
const instruction = SystemProgram.transfer({
fromPubkey: vaultSigner,
toPubkey: wallet.publicKey,
lamports: Number(parseSol(0.01)),
});
const payload = await vaultService.appendActions({
proposal,
actions: [instruction],
});
const signature = await payload.execute();Delete Proposal
const proposal = "<Proposal PublicKey>";
const payload = await vaultService.deleteProposal({ proposal });
const deleteProposalSignature = await payload.execute();Execute a Proposal
const proposal = "<Proposal PublicKey>";
const lookupTableAddress = "<Lookup Table Address PublicKey>";
const payload = await vaultService.executeProposal({
caller: wallet.publicKey,
proposal,
addressLookupTables: [lookupTableAddress], // optional
});
const signature = await payload.execute();Execute Proposal Directly (Single Transaction)
Proposal actions can be executed directly without creating a persistent proposal. However, this may hit the transaction size limit; some cases can be mitigated using address lookup tables.
import { SystemProgram, TransactionInstruction } from "@solana/web3.js";
import { parseSol } from "@zebec-network/solana-common";
import { deriveUserVault, deriveVaultSigner } from "@zebec-network/zebec-vault-sdk";
const [vault] = deriveUserVault(wallet.publicKey, vaultService.vaultV1ProgramId);
const [vaultSigner] = deriveVaultSigner(vault, vaultService.vaultV1ProgramId);
const recipient = "<Recipient PublicKey>";
const instruction1 = SystemProgram.transfer({
fromPubkey: vaultSigner,
toPubkey: recipient,
lamports: Number(parseSol(1)),
});
const instruction2 = new TransactionInstruction({
keys: [],
programId: MEMO_PROGRAM_ID,
data: Buffer.from("Transfer 1 sol from vault signer to recipient", "utf8"),
});
const payload = await vaultService.executeProposalDirect({
proposer: wallet.publicKey,
actions: [instruction1, instruction2],
addressLookupTables: [lookupTableAddress],
});
const signature = await payload.execute();Payment Streaming
Create a Stream
const payload = await vaultService.createStreamFromVault({
vaultOwner: wallet.publicKey,
receiver: recipientAddress,
streamToken: tokenMintAddress,
streamConfigName: "Config-001", // name of the stream configuration to use
amount: 1000, // total amount
duration: 2592000, // 30 days in seconds
startNow: true,
startTime: Math.floor(Date.now() / 1000),
automaticWithdrawal: false,
cancelableByRecipient: true,
cancelableBySender: true,
isPausable: true,
transferableByRecipient: false,
transferableBySender: false,
canTopup: true,
rateUpdatable: false,
cliffPercentage: 0, // 0-100
autoWithdrawFrequency: 86400, // 1 day in seconds; required when automaticWithdrawal is true
streamName: "Monthly Payment",
});
const signature = await payload.execute();Create Multiple Streams
There is a difference in the payload returned for creating multiple streams. While other payloads are instances of TransactionPayload, the payload returned for multiple streams is an instance of MultiTransactionPayload. After calling execute(), it returns MultiTransactionPayloadExecuteReturn, which is an array of settled promise results — each containing either a transaction signature on success or a failure reason.
export type MultiTransactionPayloadExecuteReturn = (PromiseSettledResult<string> & {
transactionData: {
readonly instructions: web3.TransactionInstruction[];
readonly feePayer: web3.PublicKey;
readonly signers?: web3.Signer[];
readonly addressLookupTableAccounts?: web3.AddressLookupTableAccount[];
};
transaction: web3.VersionedTransaction;
})[];const payload = await vaultService.createMultipleStreamFromVault({
vaultOwner: wallet.publicKey,
streamConfigName: "Config-001", // applies to all streams
streamInfo: [
{
receiver: recipient1,
streamToken: tokenMint,
amount: 500,
duration: 2592000,
startNow: false,
startTime: Math.floor(Date.now() / 1000) + 30,
automaticWithdrawal: false,
cancelableByRecipient: false,
cancelableBySender: false,
isPausable: false,
transferableByRecipient: false,
transferableBySender: false,
canTopup: false,
rateUpdatable: false,
cliffPercentage: 0,
streamName: "Stream 1",
},
{
receiver: recipient2,
streamToken: tokenMint,
amount: 1000,
duration: 2592000,
startNow: false,
startTime: Math.floor(Date.now() / 1000) + 30,
automaticWithdrawal: false,
cancelableByRecipient: false,
cancelableBySender: false,
isPausable: false,
transferableByRecipient: false,
transferableBySender: false,
canTopup: false,
rateUpdatable: false,
cliffPercentage: 0,
streamName: "Stream 2",
},
],
});
// Execute all streams in separate transactions
const results = await payload.execute();Pause/Resume a Stream
const payload = await vaultService.pauseResumeStream({
vaultOwner: wallet.publicKey,
streamMetadata: streamAddress,
});
const signature = await payload.execute();Cancel a Stream
const payload = await vaultService.cancelStream({
vaultOwner: wallet.publicKey,
streamMetadata: streamAddress,
});
await payload.execute();Withdraw from Stream
const payload = await vaultService.withdrawStream({
vaultOwner: wallet.publicKey,
streamMetadata: streamAddress,
});
await payload.execute();Change Stream Receiver
const payload = await vaultService.changeStreamReceiver({
vaultOwner: wallet.publicKey,
streamMetadata: streamAddress,
newRecipient: newRecipientAddress,
});
await payload.execute();Get Stream Information
const streamInfo = await vaultService.getStreamMetadataInfo(streamAddress);
console.log("Stream details:", {
sender: streamInfo.parties.sender,
receiver: streamInfo.parties.receiver,
token: streamInfo.financials.streamToken,
deposited: streamInfo.financials.depositedAmount,
withdrawn: streamInfo.financials.withdrawnAmount,
startTime: new Date(streamInfo.schedule.startTime * 1000),
endTime: new Date(streamInfo.schedule.endTime * 1000),
isPaused: streamInfo.schedule.pausedTimestamp > 0,
});Staking
Stake Tokens
const payload = await vaultService.stake({
lockupName: "main-lockup",
vaultOwner: wallet.publicKey,
amount: 1000,
lockPeriod: 7776000, // 90 days in seconds; must be one of the lockup's valid periods
nonce: 0n,
});
await payload.execute();Unstake Tokens
const payload = await vaultService.unstake({
lockupName: "main-lockup",
vaultOwner: wallet.publicKey,
nonce: 0n,
});
await payload.execute();Get Stake Nonce Information
const nonceInfo = await vaultService.getStakeUserNonceInfo("main-lockup", wallet.publicKey);
console.log("Current nonce:", nonceInfo ? nonceInfo.nonce : 0n);Zebec Cards
Create a Silver Card
const nextIndex = await vaultService.getNextCardIndex();
const payload = await vaultService.createSilverCard({
vaultOwnerAddress: wallet.publicKey,
nextCardIndex: nextIndex,
amount: 100, // USDC amount
usdcAddress: USDC_MINT_ADDRESS,
emailHash: Buffer.from("your-32-byte-hash"), // must be exactly 32 bytes
currency: "USD",
});
await payload.execute();Load a Carbon Card
const nextIndex = await vaultService.getNextCardIndex();
const payload = await vaultService.loadCarbonCard({
vaultOwnerAddress: wallet.publicKey,
nextCardIndex: nextIndex,
amount: 50,
usdcAddress: USDC_MINT_ADDRESS,
emailHash: Buffer.from("your-32-byte-hash"), // must be exactly 32 bytes
currency: "USD",
reloadCardId: "CARD_ID",
});
await payload.execute();Swap and Create Silver Card
// First, get a Jupiter quote
const quoteParams = new URLSearchParams({
inputMint: inputMint,
outputMint: USDC_MINT_ADDRESS,
amount: parsedAmount, // in smallest units
slippageBps: "50",
swapMode: "ExactIn",
});
const quoteResponse = await fetch(
`https://lite-api.jup.ag/swap/v1/quote?${quoteParams}`,
);
const quoteInfo = await quoteResponse.json();
const nextIndex = await vaultService.getNextCardIndex();
const payload = await vaultService.swapAndCreateSilverCard({
vaultOwnerAddress: wallet.publicKey,
quoteInfo: quoteInfo,
nextCardCounter: nextIndex,
emailHash: Buffer.from("your-32-byte-hash"), // must be exactly 32 bytes
currency: "USD",
wrapAndUnwrapSol: true, // set to true when swapping SOL
});
await payload.execute();Swap and Load Carbon Card
const payload = await vaultService.swapAndLoadCarbonCard({
vaultOwnerAddress: wallet.publicKey,
quoteInfo: quoteInfo,
nextCardCounter: nextIndex,
emailHash: Buffer.from("your-32-byte-hash"), // must be exactly 32 bytes
currency: "USD",
reloadCardId: "CARD_ID",
wrapAndUnwrapSol: true,
});
await payload.execute();Get Card Custom Token Fees
const tokenFees = await vaultService.getCardCustomTokenFees();
tokenFees.forEach((fee) => {
console.log(`Token: ${fee.tokenAddress}, Fee: ${fee.fee}%`);
});Read-only Provider
For read-only operations without a wallet:
import { createReadonlyProvider } from "@zebec-network/zebec-vault-sdk";
const readonlyProvider = createReadonlyProvider(connection, optionalWalletAddress);
const service = ZebecVaultService.create(readonlyProvider, "mainnet-beta");
// Can only call query methods
const vaultInfo = await service.getVaultInfoOfUser(someAddress);Advanced Features
Program Derived Addresses (PDAs)
import {
deriveUserVault,
deriveVaultSigner,
} from "@zebec-network/zebec-vault-sdk";
const [vaultAddress, vaultBump] = deriveUserVault(userAddress, vaultProgramId);
const [signerAddress, signerBump] = deriveVaultSigner(vaultAddress, vaultProgramId);Transaction Utilities
import { calculateProposalSize, transactionInstructionToProposalAction } from "@zebec-network/zebec-vault-sdk";
// Calculate proposal size (in bytes)
const size = calculateProposalSize("proposal-name", actions);
// Convert a TransactionInstruction to a ProposalAction
const action = transactionInstructionToProposalAction(instruction);Network Configuration
// Mainnet
const mainnetService = ZebecVaultService.create(provider, "mainnet-beta");
// Devnet
const devnetService = ZebecVaultService.create(provider, "devnet");Constants
import {
ZEBEC_VAULT_PROGRAM_ID,
JUPITER_AGGREGATOR_PROGRAM_ID,
CARD_LOOKUP_TABLE_ADDRESS,
STAKE_LOOKUP_TABLE_ADDRESS,
} from "@zebec-network/zebec-vault-sdk";
console.log("Vault Program:", ZEBEC_VAULT_PROGRAM_ID["mainnet-beta"]);Error Types
The SDK throws typed errors you can catch and inspect:
import {
AmountOutOfRangeError,
DailyCardLimitReachedError,
InvalidUsdcAddressError,
NotEnoughBalanceError,
QuoteResponseError,
AssociatedTokenAccountDoesNotExistsError,
} from "@zebec-network/zebec-vault-sdk";
try {
await payload.execute();
} catch (err) {
if (err instanceof AmountOutOfRangeError) {
console.error(`Amount must be between ${err.minRange} and ${err.maxRange}`);
} else if (err instanceof DailyCardLimitReachedError) {
console.error(`Daily limit: ${err.dailyCardLimit}, requested: ${err.requestedAmount}`);
} else if (err instanceof NotEnoughBalanceError) {
console.error("Insufficient balance");
} else if (err instanceof AssociatedTokenAccountDoesNotExistsError) {
console.error("Token account does not exist");
}
}Types
The SDK exports comprehensive TypeScript types:
import type {
VaultInfo,
ProposalInfo,
ProposalAction,
ProposalStage,
CreateStreamFromVaultParams,
createMultipleStreamFromVaultParams,
StreamMetadataInfo,
TokenFeeRecord,
QuoteInfo,
RouteInfo,
StakeUserNonceInfo,
CreateSilverCardParams,
LoadCarbonCardParams,
SwapAndCreateSilverCardParams,
SwapAndLoadCarbonCardParams,
CancelStreamParams,
PauseResumeStreamParams,
ChangeStreamReceiverParams,
WithdrawStreamParams,
ParsedCardConfigInfo,
WhitelistInfo,
Numeric,
} from "@zebec-network/zebec-vault-sdk";Best Practices
- Always check balances before operations
- Use proposals for critical operations that require multiple instructions and cannot be executed in a single transaction
- Handle errors appropriately with the provided error types
- Verify stream parameters before creation (duration, amounts, permissions)
- Test on
devnetbefore deploying tomainnet - Use address lookup tables for complex transactions to reduce transaction size
- Use a private dedicated RPC for production workloads
