Complete reference for the @vaultdao/sdk TypeScript package.
- Installation
- Quick Start
- Authentication
- Contract Functions
- Types
- Error Codes
- Events
- Integration Guide
npm install @vaultdao/sdkPeer dependency for browser (signing): Freighter browser extension.
import {
buildOptions,
connectWallet,
proposeTransfer,
signAndSubmit,
} from "@vaultdao/sdk";
const opts = buildOptions("testnet", "CXXXXXXX...");
const wallet = await connectWallet();
const txXdr = await proposeTransfer(
wallet.publicKey,
"GDEST...",
"CDLZFC3...",
BigInt(1e7),
"memo",
opts,
);
const hash = await signAndSubmit(txXdr, opts);Connects to the Freighter browser extension. Throws if Freighter is not installed.
const wallet = await connectWallet();
// { publicKey: "GABC...", network: "TESTNET", networkUrl: "https://..." }Creates the options object required by every SDK function.
| Parameter | Type | Description |
|---|---|---|
network |
Network |
"testnet", "mainnet", "futurenet" |
contractId |
string |
Deployed contract Strkey (Cxxx...) |
All write functions return a prepared transaction XDR string. Pass this to signAndSubmit() to broadcast.
Initialise the vault. Can only be called once.
| Parameter | Type | Description |
|---|---|---|
adminPublicKey |
string |
Admin's Stellar address |
config |
InitConfig |
Full configuration (see Types) |
opts |
SdkOptions |
Connection options |
Errors: AlreadyInitialized, NoSigners, ThresholdTooLow, ThresholdTooHigh, InvalidAmount
const txXdr = await initialize(
adminPublicKey,
{
signers: [admin, signer1, signer2],
threshold: 2,
spendingLimit: BigInt(1000e7),
dailyLimit: BigInt(5000e7),
weeklyLimit: BigInt(10000e7),
timelockThreshold: BigInt(500e7),
timelockDelay: BigInt(17280), // ~1 day
},
opts,
);
const hash = await signAndSubmit(txXdr, opts);Create a new transfer proposal. Proposer must have Treasurer or Admin role.
| Parameter | Type | Description |
|---|---|---|
proposerPublicKey |
string |
Proposer's address |
recipient |
string |
Destination Stellar address |
tokenAddress |
string |
Contract ID of the token to transfer |
amount |
bigint |
Amount in stroops (smallest unit) |
memo |
string |
Short memo, ≤ 32 chars, no spaces |
opts |
SdkOptions |
Connection options |
Returns: string — prepared transaction XDR
Errors: InsufficientRole, InvalidAmount, ExceedsProposalLimit, ExceedsDailyLimit, ExceedsWeeklyLimit
Cast an approval vote. The proposal transitions to Approved once the threshold is reached.
| Parameter | Type | Description |
|---|---|---|
signerPublicKey |
string |
Signer's address |
proposalId |
bigint |
Target proposal ID |
opts |
SdkOptions |
Connection options |
Errors: NotASigner, InsufficientRole, ProposalNotPending, AlreadyApproved, ProposalExpired
Execute an Approved proposal, transferring funds to the recipient.
| Parameter | Type | Description |
|---|---|---|
executorPublicKey |
string |
Executor's address |
proposalId |
bigint |
Target proposal ID |
opts |
SdkOptions |
Connection options |
Errors: ProposalNotApproved, ProposalAlreadyExecuted, TimelockNotExpired, InsufficientBalance, ProposalExpired
Note: If
unlockLedger > 0, wait until the current ledger sequence exceeds it before calling.
Cancel a Pending proposal. Only the original proposer or an Admin may reject.
Errors: Unauthorized, ProposalNotPending
Assign a Role to any address. Only Admin can call this.
| Parameter | Type | Description |
|---|---|---|
adminPublicKey |
string |
Admin's address |
targetAddress |
string |
Address to assign the role to |
role |
Role |
Role.Member, Role.Treasurer, Role.Admin |
Errors: Unauthorized
Add a new address to the signers list. Only Admin.
Errors: Unauthorized, SignerAlreadyExists
Remove a signer. Fails if removal would make the threshold unreachable.
Errors: Unauthorized, SignerNotFound, CannotRemoveSigner
Update per-proposal and daily spending limits. Only Admin.
Errors: Unauthorized, InvalidAmount
Change the M-of-N approval threshold. Must satisfy 1 ≤ threshold ≤ signers.length.
Errors: Unauthorized, ThresholdTooLow, ThresholdTooHigh
Schedule a recurring automatic payment. Minimum interval is 720 ledgers (~1 hour).
| Parameter | Type | Description |
|---|---|---|
intervalLedgers |
bigint |
Cadence in ledgers (720 min; 17280 ≈ 1 day) |
Errors: InsufficientRole, InvalidAmount, IntervalTooShort
Execute a due recurring payment. Anyone (keeper bot) can call this.
Errors: TimelockNotExpired (not yet due), ExceedsDailyLimit, InsufficientBalance
| Function | Description | Returns |
|---|---|---|
getProposal(id, callerKey, opts) |
Fetch proposal by ID | Proposal |
getRole(address, callerKey, opts) |
Get role for address | Role |
getTodaySpent(callerKey, opts) |
Today's aggregate spending | bigint |
isSigner(address, callerKey, opts) |
Is address a signer? | boolean |
interface InitConfig {
signers: string[]; // List of signer addresses
threshold: number; // M in M-of-N
spendingLimit: bigint; // Max per proposal (stroops)
dailyLimit: bigint; // Max daily aggregate (stroops)
weeklyLimit: bigint; // Max weekly aggregate (stroops)
timelockThreshold: bigint; // Amount triggering timelock (stroops)
timelockDelay: bigint; // Timelock duration in ledgers
}interface Proposal {
id: bigint;
proposer: string;
recipient: string;
token: string;
amount: bigint;
memo: string;
approvals: string[];
status: ProposalStatus;
createdAt: bigint;
expiresAt: bigint;
unlockLedger: bigint; // 0 = no timelock
}| Value | Numeric | Permissions |
|---|---|---|
Role.Member |
0 |
Read-only |
Role.Treasurer |
1 |
Propose and approve transfers |
Role.Admin |
2 |
Full control |
| Value | Numeric | Meaning |
|---|---|---|
ProposalStatus.Pending |
0 |
Awaiting approvals |
ProposalStatus.Approved |
1 |
Threshold met, ready to execute |
ProposalStatus.Executed |
2 |
Funds transferred |
ProposalStatus.Rejected |
3 |
Cancelled |
ProposalStatus.Expired |
4 |
Expired without reaching threshold |
All contract errors surface as VaultError instances with a .code property.
import { parseError, VaultError, VaultErrorCode } from "@vaultdao/sdk";
try {
await proposeTransfer(/* ... */);
} catch (err) {
const parsed = parseError(err);
if (parsed instanceof VaultError) {
console.error(parsed.code, VaultErrorCode[parsed.code]);
}
}| Code | Name | Description | How to handle |
|---|---|---|---|
| 100 | AlreadyInitialized |
Contract already set up | Don't call initialize() twice |
| 101 | NotInitialized |
Contract not yet set up | Call initialize() first |
| 200 | Unauthorized |
Caller not authorised for this action | Check your role |
| 201 | NotASigner |
Address not in the signers list | Add via addSigner() |
| 202 | InsufficientRole |
Role too low for this action | Elevate with setRole() |
| 300 | ProposalNotFound |
Proposal ID doesn't exist | Verify the ID |
| 301 | ProposalNotPending |
Proposal not in Pending state | Check status first |
| 302 | AlreadyApproved |
Signer already voted | Each signer votes once |
| 303 | ProposalExpired |
Proposal lifetime exceeded (~7 days) | Create a new proposal |
| 304 | ProposalNotApproved |
Threshold not met | Wait for more approvals |
| 305 | ProposalAlreadyExecuted |
Proposal was already executed | Nothing to do |
| 400 | ExceedsProposalLimit |
Amount > per-proposal limit | Reduce amount or increase limit |
| 401 | ExceedsDailyLimit |
Daily cap would be exceeded | Wait until next day |
| 402 | ExceedsWeeklyLimit |
Weekly cap would be exceeded | Wait until next week |
| 403 | InvalidAmount |
Amount ≤ 0 | Use a positive amount |
| 404 | TimelockNotExpired |
Timelock still active | Wait until unlockLedger |
| 405 | IntervalTooShort |
Recurring interval < 720 ledgers | Use ≥ 720 ledgers |
| 500 | ThresholdTooLow |
Threshold < 1 | Use threshold ≥ 1 |
| 501 | ThresholdTooHigh |
Threshold > number of signers | Add more signers |
| 502 | SignerAlreadyExists |
Address already a signer | No duplicate signers |
| 503 | SignerNotFound |
Address not in signers list | Check address |
| 504 | CannotRemoveSigner |
Removal would break threshold | Reduce threshold first |
| 505 | NoSigners |
Empty signers list | Provide at least one signer |
| 600 | TransferFailed |
Token transfer failed | Check token contract |
| 601 | InsufficientBalance |
Vault balance too low | Top up the vault |
The contract emits the following Soroban events that can be indexed by horizon or a custom listener.
| Topic | Additional Data | Emitted by |
|---|---|---|
initialized |
(admin, threshold) |
initialize() |
proposal_created + proposalId |
(proposer, recipient, amount) |
proposeTransfer() |
proposal_approved + proposalId |
(approver, approvalCount, threshold) |
approveProposal() |
proposal_ready + proposalId |
— | approveProposal() (on threshold met) |
proposal_executed + proposalId |
(executor, recipient, amount) |
executeProposal() |
proposal_rejected + proposalId |
rejector |
rejectProposal() |
role_assigned |
(address, roleNumeric) |
setRole() |
config_updated |
updaterAddress |
updateLimits(), updateThreshold() |
signer_added |
(signer, totalSigners) |
addSigner() |
signer_removed |
(signer, totalSigners) |
removeSigner() |
import {
buildOptions,
connectWallet,
proposeTransfer,
signAndSubmit,
parseError,
VaultError,
} from "@vaultdao/sdk";
import { useState } from "react";
const opts = buildOptions("testnet", import.meta.env.VITE_CONTRACT_ID);
export function ProposeButton({
recipient,
amount,
}: {
recipient: string;
amount: bigint;
}) {
const [status, setStatus] = useState("");
const handlePropose = async () => {
try {
const wallet = await connectWallet();
const txXdr = await proposeTransfer(
wallet.publicKey,
recipient,
TOKEN_ID,
amount,
"memo",
opts,
);
const hash = await signAndSubmit(txXdr, opts);
setStatus(`Proposal submitted: ${hash}`);
} catch (err) {
const parsed = parseError(err);
setStatus(
parsed instanceof VaultError
? `Error: ${parsed.message}`
: "Unknown error",
);
}
};
return <button onClick={handlePropose}>Propose Transfer — {status}</button>;
}// keeper.ts — automatically execute due recurring payments
import {
buildOptions,
executeRecurringPayment,
signAndSubmit,
} from "@vaultdao/sdk";
import { Keypair } from "stellar-sdk";
const opts = buildOptions("testnet", process.env.CONTRACT_ID!);
const keeper = Keypair.fromSecret(process.env.KEEPER_SECRET!);
async function runKeeper(paymentId: bigint) {
// Build unsigned transaction
const txXdr = await executeRecurringPayment(
keeper.publicKey(),
paymentId,
opts,
);
// Sign locally (no Freighter in Node.js)
const { Transaction } = await import("stellar-sdk");
const tx = new Transaction(txXdr, opts.networkPassphrase);
tx.sign(keeper);
// Submit
const { SorobanRpc } = await import("stellar-sdk");
const server = new SorobanRpc.Server(opts.rpcUrl);
await server.sendTransaction(tx);
}Poll for proposal approval:
async function waitForApproval(
proposalId: bigint,
callerKey: string,
opts: SdkOptions,
) {
while (true) {
const proposal = await getProposal(proposalId, callerKey, opts);
if (proposal.status !== ProposalStatus.Pending) return proposal;
await new Promise((r) => setTimeout(r, 5000)); // poll every 5 s
}
}Check timelock before executing:
import { SorobanRpc } from "stellar-sdk";
async function canExecute(
proposalId: bigint,
callerKey: string,
opts: SdkOptions,
) {
const proposal = await getProposal(proposalId, callerKey, opts);
if (proposal.status !== ProposalStatus.Approved) return false;
if (proposal.unlockLedger === BigInt(0)) return true;
const server = new SorobanRpc.Server(opts.rpcUrl);
const ledger = await server.getLatestLedger();
return BigInt(ledger.sequence) >= proposal.unlockLedger;
}