This guide documents the SDK helpers that talk to the PocketPay Savings Vault
Soroban smart contract: depositToVault, withdrawFromVault, and
getVaultBalance. It also explains how the VAULT_CONTRACT_ID is resolved, how
each helper maps onto the on-chain contract, and — importantly — the current
limitations of that contract so you know exactly what these calls do and don't
do today.
The contract these helpers target lives in
Axionvera/pocketpay-contracts
under contracts/savings_vault. This document is kept aligned with that source.
- Prerequisites
- The
VAULT_CONTRACT_ID - Configuration & Network
- Helpers
- How helpers map to the contract
- Current contract limitations
- Error handling
- Full example
Before any vault helper will work you need:
- A deployed, initialized Savings Vault contract on the network you're
targeting (Testnet by default). Deploy it from the
pocketpay-contractsrepo, then call itsinitialize(admin)method once. The SDK does not wrap deployment or initialization — it assumes the contract already exists. - The contract's ID (a
C...strkey), provided to the SDK via a parameter or theVAULT_CONTRACT_IDenvironment variable (see below). - A funded source account for deposits/withdrawals — the account whose
secret key signs the transaction must exist on-network. On Testnet you can
fund it with
fundTestnetAccount(publicKey).
Every vault helper needs to know which deployed contract to call. The ID is
resolved by resolveContractId() with this precedence:
- The
contractIdpassed in the params object, if present. - Otherwise,
SDKConfig.contractIdfrom the config argument. - Otherwise, the
VAULT_CONTRACT_IDenvironment variable. - Otherwise, the
STELLAR_CONTRACT_IDenvironment variable. - If none is set, the call throws a
CapabilityMismatchError(a subclass ofPocketPayError) with codeVAULT_CONTRACT_NOT_CONFIGURED.
// Option A — pass it explicitly
await getVaultBalance({
publicKey: 'GABC...',
contractId: 'CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC',
});
// Option B — set it once in the environment
// .env
// VAULT_CONTRACT_ID=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
await getVaultBalance({ publicKey: 'GABC...' });Type note: In
VaultDepositParams/VaultWithdrawParams/VaultBalanceParamsthecontractIdfield is typed as a requiredstring, but at runtime the helpers fall back toVAULT_CONTRACT_IDwhen it is omitted. If you rely on the environment variable, you may need to cast or provide the field to satisfy the type checker.
Vault helpers accept an optional second argument, config?: Partial<SDKConfig>,
which is resolved by resolveConfig(). The relevant field for Soroban is the
RPC URL, which defaults per-network:
| Network | Default Soroban RPC URL |
|---|---|
testnet |
https://soroban-testnet.stellar.org |
mainnet |
https://soroban.stellar.org |
You can override it per-call, via STELLAR_SOROBAN_RPC_URL, or via
STELLAR_NETWORK. The default network is testnet.
await depositToVault(params, {
network: 'testnet',
sorobanRpcUrl: 'https://my-custom-rpc.example.com',
});All three helpers return a VaultResult:
interface VaultResult {
success: boolean; // whether the operation succeeded
hash?: string; // transaction hash (deposit / withdraw only)
balance?: string; // resulting balance in XLM (getVaultBalance only)
error?: string; // human-readable error, when success is false
}Note the two failure styles: expected on-chain failures (a failed
simulation, a rejected send, a non-SUCCESS status) are returned as
{ success: false, error }, whereas programmer / environment errors
(missing contract ID, invalid keys, RPC unreachable) are thrown as
PocketPayError. Handle both — wrap calls in try/catch and check success.
depositToVault(
params: VaultDepositParams,
config?: Partial<SDKConfig>
): Promise<VaultResult>Builds, simulates, signs, and submits a call to the contract's deposit
method, then polls until the transaction resolves.
| Param | Type | Description |
|---|---|---|
sourceSecret |
string |
Secret key of the depositor (signs the tx). |
amount |
string |
Amount of XLM to deposit, e.g. '100'. |
contractId |
string |
Vault contract ID (or set via env var). |
The amount is validated by validateAmount and converted to an integer with
Math.round(parseFloat(amount) * 10_000_000) (a stroops-style representation)
before being passed to the contract as an i128.
const result = await depositToVault({
sourceSecret: wallet.secretKey,
amount: '100',
contractId,
});
if (result.success) {
console.log('Deposited, tx hash:', result.hash);
} else {
console.error('Deposit failed:', result.error);
}withdrawFromVault(
params: VaultWithdrawParams,
config?: Partial<SDKConfig>
): Promise<VaultResult>Same flow as depositToVault but calls the contract's withdraw method. The
contract rejects the call (simulation fails) if amount exceeds the caller's
available balance or is not greater than zero.
| Param | Type | Description |
|---|---|---|
sourceSecret |
string |
Secret key of the withdrawer (signs the tx). |
amount |
string |
Amount of XLM to withdraw, e.g. '50'. |
contractId |
string |
Vault contract ID (or set via env var). |
const result = await withdrawFromVault({
sourceSecret: wallet.secretKey,
amount: '50',
contractId,
});getVaultBalance(
params: VaultBalanceParams,
config?: Partial<SDKConfig>
): Promise<VaultResult>Queries the caller's available (unlocked) balance. This is a read-only
operation: it calls the contract's get_balance method and reads the return
value from the transaction simulation — nothing is signed or submitted to the
network, so it costs no fees.
| Param | Type | Description |
|---|---|---|
publicKey |
string |
Public key of the user to query. |
contractId |
string |
Vault contract ID (or set via env var). |
The raw i128 returned by the contract is divided by 10_000_000 and returned
as a fixed 7-decimal XLM string in result.balance. If the user has never
deposited, the contract returns 0 and balance is '0.0000000'.
const result = await getVaultBalance({ publicKey: wallet.publicKey, contractId });
if (result.success) console.log('Vault balance:', result.balance, 'XLM');Even though
get_balanceis read-only, the SDK still callsgetAccount(publicKey)to build the simulation transaction, so the queried account must exist on-network.
The Savings Vault contract exposes seven methods. The SDK currently wraps three of them:
Contract method (savings_vault) |
SDK helper | Notes |
|---|---|---|
deposit(user, amount) |
depositToVault |
amount is i128; user auth required. |
withdraw(user, amount) |
withdrawFromVault |
Fails if amount > available balance. |
get_balance(user) -> i128 |
getVaultBalance |
Read-only, via simulation. |
initialize(admin) |
— (not wrapped) | One-time, admin-only. Do it out-of-band. |
lock_funds(user, amount, unlock_time) |
— (not wrapped) | Time-locks part of the balance. |
get_locked_balance(user) -> i128 |
— (not wrapped) | Returns locked (not available) balance. |
can_withdraw(user) -> bool |
— (not wrapped) | True once the unlock timestamp has passed. |
Auth model: the contract's deposit / withdraw call user.require_auth().
The SDK satisfies this by signing the prepared transaction with the
sourceSecret keypair, so the signer and the user address must match.
Raw contract responses from Soroban RPC nodes (such as simulation outputs, XDR payloads, or status flags) can be complex for application logic and mobile clients to handle directly. The SDK provides standard result mappers (mapSorobanInvocationResult, mapVaultInvocationResult, and mapSorobanContractError) that map raw contract outputs into stable, typed values.
import {
mapSorobanInvocationResult,
mapVaultInvocationResult,
mapSorobanContractError,
SorobanInvocationResult,
VaultMappedResult,
} from 'stellar-pocketpay-sdk';
// Generic Soroban Invocation Result
interface SorobanInvocationResult<T = unknown> {
success: boolean; // High-level success flag
status: 'success' | 'failed' | 'simulation_error' | 'error' | 'pending';
result?: T; // Parsed contract return value
error?: string; // Formatted error message
errorCode?: string | number; // Classified error code
hash?: string; // Transaction hash if submitted
rawResponse?: unknown; // Raw RPC response object
}
// Vault-specific Mapped Result (returned by depositToVault, withdrawFromVault, getVaultBalance)
interface VaultMappedResult {
success: boolean;
status: 'success' | 'failed' | 'simulation_error' | 'error' | 'pending';
operation: 'deposit' | 'withdraw' | 'get_balance';
hash?: string;
balance?: string; // Formatted XLM balance string (e.g. "15.0000000")
rawStroops?: string; // Raw sub-unit balance (e.g. "150000000")
amount?: string; // Amount requested (deposit/withdraw)
error?: string;
errorCode?: string | number;
}import { mapSorobanInvocationResult, mapVaultInvocationResult } from 'stellar-pocketpay-sdk';
// 1. Mapping a raw Soroban simulation response
const simResponse = await sorobanServer.simulateTransaction(tx);
const mappedSim = mapSorobanInvocationResult(simResponse);
if (mappedSim.success) {
console.log('Parsed return value:', mappedSim.result);
} else {
console.error('Simulation error:', mappedSim.error, 'Code:', mappedSim.errorCode);
}
// 2. Mapping vault invocation responses
const mappedVault = mapVaultInvocationResult('get_balance', simResponse, { contractId });
console.log('Balance XLM:', mappedVault.balance, 'Raw Stroops:', mappedVault.rawStroops);These are real limitations of the contract as it exists today. Read them before building anything on top of these helpers.
-
The vault is internal bookkeeping, not token custody. The contract's
depositsimply increments a storedi128balance for the user, andwithdrawdecrements it. It does not perform any token transfer — no XLM or SAC token actually moves into or out of the contract. Depositing does not remove funds from your wallet, and withdrawing does not send funds back. The stored balance is an accounting number only. Do not treat these helpers as moving real value until the contract integrates a token transfer. -
Amounts are unitless integers on-chain. The contract stores whatever
i128it is given; it has no concept of decimals or stroops. The× 10_000_000/÷ 10_000_000conversion is a convention applied by the SDK only. Every writer of that balance must use the same convention, or reads will be off by orders of magnitude. -
Only 3 of 7 methods are wrapped. There are no SDK helpers for
initialize,lock_funds,get_locked_balance, orcan_withdraw. If you need fund-locking, you must call the contract directly (e.g. via the Stellar SDK or Soroban CLI) for now. -
getVaultBalancereports only the available balance. It callsget_balance, which excludes locked funds. If funds have been locked vialock_funds(out-of-band), they will not appear inresult.balance; queryget_locked_balancedirectly to see them. -
No on-chain events. The contract emits diagnostic
log!output, notenv.events(). There is nothing to subscribe to for deposit/withdraw notifications — the SDK relies on the transaction hash and status instead. -
The contract must be initialized first. A freshly deployed contract must have
initialize(admin)called exactly once before use. The SDK does not do this and does not check for it.
Vault-relevant error codes carried on PocketPayError.code:
| Code | When it occurs |
|---|---|
VAULT_CONTRACT_NOT_CONFIGURED |
No contract ID from params, SDKConfig.contractId, or env. |
INVALID_SECRET_KEY |
sourceSecret is not a valid Stellar secret key. |
INVALID_PUBLIC_KEY |
publicKey is not a valid Stellar public key. |
INVALID_AMOUNT |
Amount is non-numeric or not greater than zero. |
VAULT_DEPOSIT_ERROR |
An unexpected error was thrown during a deposit. |
VAULT_WITHDRAW_ERROR |
An unexpected error was thrown during a withdrawal. |
VAULT_BALANCE_ERROR |
An unexpected error was thrown during a balance query. |
import { depositToVault, PocketPayError } from 'stellar-pocketpay-sdk';
try {
const result = await depositToVault(params);
if (!result.success) {
// expected on-chain failure (e.g. simulation rejected)
console.error('On-chain failure:', result.error);
}
} catch (error) {
if (error instanceof PocketPayError) {
console.error(error.code, error.message);
}
}A complete, runnable script lives at
examples/vault-operations.ts:
VAULT_CONTRACT_ID=CXXXXX npx tsx examples/vault-operations.tsIt creates and funds a Testnet wallet, deposits into the vault, queries the balance, and withdraws — a good end-to-end reference for wiring these helpers together.