Framework-agnostic TypeScript SDK for Stellar.
The execution layer for wallet connection, transaction handling,
and Soroban smart contract interaction — with a no-throw result model throughout.
Part of the sorokit ecosystem.
sorokit-core gives you a single typed client for everything you need to build on Stellar: connecting wallets, reading accounts, building and submitting transactions, and invoking Soroban contracts. Every function returns a SorokitResult<T> — no try/catch, no uncaught promise rejections, no surprises.
It is deliberately stateless and framework-agnostic. It runs in Node, the browser, React, Vue, Svelte, or any environment that can execute TypeScript — with no opinion about how you manage state.
- Installation
- Quick Start
- Modules
- API Reference
- Result Type
- Wallet Adapters
- Network Management
- Streaming
- Networks
- Testing Utilities
- Examples
- Recipes and Cookbook
- Workflow and Architecture Guides
- New in This Release
- Design Principles
- License
npm install sorokit-core @creit.tech/stellar-wallets-kit@creit.tech/stellar-wallets-kit is a required peer dependency. It provides the underlying wallet adapter infrastructure that sorokit-core builds on.
import { createSorokitClient, FreighterAdapter } from "sorokit-core";
// 1. Create a client
const result = createSorokitClient({ network: "testnet" });
if (result.status === "error") throw new Error(result.error.message);
const client = result.data;
// 2. Connect a wallet
const adapter = new FreighterAdapter(swkInstance);
const conn = await client.wallet.connect(adapter);
if (conn.status === "error") throw new Error(conn.error.message);
const { publicKey } = conn.data;
// 3. Fetch account balances
const account = await client.account.get(publicKey);
if (account.status === "ok") {
console.log(account.data.balances);
}
// 4. Build, sign, and submit a payment
const tx = await client.transaction.buildPayment(publicKey, {
destination: "GDEST...WXYZ",
amount: "10",
});
if (tx.status === "ok") {
const signed = await client.wallet.signTransaction(adapter, {
transactionXdr: tx.data,
networkPassphrase: client.networkConfig.networkPassphrase,
});
if (signed.status === "ok") {
await client.transaction.submit(signed.data);
}
}| Module | Responsibility |
|---|---|
wallet |
Connect/disconnect wallets, sign transactions, offline signing, wallet status tracking |
account |
Fetch account info, balances, stream state, balance alerts, activity summaries, key rotation, sponsorship |
transaction |
Build/submit/track transactions, fee estimation, multi-sig support, path payments, history export, webhooks |
soroban |
Read/invoke contracts, simulate transactions, contract interaction builder, event decoding, deploy validation |
network |
Network configuration, circuit breaker, dynamic network switching, network resolution |
shared |
Logging, tracing, caching, configuration, validation, server factory |
The hosted API reference is published by
GitHub Actions after a successful deployment. It documents the public exports from
src/index.ts, including parameter descriptions, return values, and examples.
Generate and view it locally:
npm ci --legacy-peer-deps
npm run docsOpen docs/api/index.html in a browser. Generated HTML is ignored by Git.
The documentation workflow validates pull requests, and publishes on pushes to
main, published releases, and manual runs. To enable hosting for this repository,
select Settings → Pages → Build and deployment → Source → GitHub Actions,
then run the workflow. The workflow also preserves generated HTML on gh-pages.
Ensure the github-pages environment permits the branches and release tags you
intend to deploy. For a fork, use
https://<owner>.github.io/<repository>/ and update the hosted link above.
Documentation is only live after the workflow and GitHub Pages deployment succeed.
client.wallet.connect(adapter); // → SorokitResult<WalletState>
client.wallet.disconnect(adapter); // → SorokitResult<WalletState>
client.wallet.signTransaction(adapter, input); // → SorokitResult<string>
client.wallet.emptyState(); // → SorokitResult<WalletState>// Fetch full account info
client.account.get(publicKey); // → SorokitResult<AccountInfo>
// Fetch all balances
client.account.getBalances(publicKey); // → SorokitResult<AssetBalance[]>
// Filter balances by asset code, issuer, type, or exclude zero balances
client.account.getAssetBalances(publicKey, {
assetCode: "USDC",
assetIssuer: "GA5Z...",
excludeZero: true,
}); // → SorokitResult<AssetBalance[]>
// Get account activity summary (new)
client.account.getAccountActivitySummary(publicKey); // → SorokitResult<ActivitySummary>
// Create balance alerts (new)
client.account.createBalanceAlert(publicKey, {
minBalance: "100",
onAlert: (balance) => console.log("Low balance alert"),
});
// Rotate account keys (new)
client.account.rotateKey(publicKey, params); // → SorokitResult<Transaction>
// Manage account sponsorship (new)
client.account.sponsorAccount(publicKey, sponsorKey); // → SorokitResult<Transaction>
// Poll Horizon and stream account state changes
for await (const result of client.account.stream(publicKey)) {
if (result.status === "ok") console.log(result.data.balances);
}// Build common transaction types (returns XDR string)
client.transaction.buildPayment(sourceKey, params); // → SorokitResult<string>
client.transaction.buildCreateAccount(sourceKey, params); // → SorokitResult<string>
client.transaction.buildTrustline(sourceKey, params); // → SorokitResult<string>
// Build multi-signature transactions (new)
client.transaction.buildMultiSigTransaction(sourceKey, params); // → SorokitResult<string>
// Build path payments (new)
client.transaction.buildPathPayment(sourceKey, params); // → SorokitResult<string>
// Submit and query
client.transaction.submit(signedXdr); // → SorokitResult<TransactionResult>
client.transaction.getStatus(hash); // → SorokitResult<TransactionResult>
// Estimate fee from a pre-built XDR
client.transaction.estimateFee({ kind: "xdr", transactionXdr: xdr });
// Or estimate from payment params directly
client.transaction.estimateFee({
kind: "payment",
publicKey,
destination: "GDEST...",
amount: "10",
}); // → SorokitResult<FeeEstimate>
// Get fee analytics (new)
client.transaction.getFeeAnalytics(params); // → SorokitResult<FeeAnalytics>
// Query transaction history (new)
client.transaction.queryTransactionHistory(publicKey, { limit: 50 }); // → SorokitResult<Transaction[]>
// Export transaction history (new)
client.transaction.exportTransactionHistory(publicKey, format); // → SorokitResult<string>
// Validate destination (new)
client.transaction.validateDestination("GDEST..."); // → SorokitResult<boolean>
// Validate transaction offline (new)
client.transaction.validateTransactionOffline(transactionXdr); // → SorokitResult<ValidationResult>
// Stream transactions for an account
for await (const result of client.transaction.stream(publicKey)) {
if (result.status === "ok") console.log(result.data.transactions);
}The SDK also exposes module bundles for applications that only need one part of the API:
import { FreighterAdapter } from "sorokit-core/wallet";
import { buildSetOptionsTransaction } from "sorokit-core/transaction";
import { getOffers } from "sorokit-core/account";These subpaths emit independent ESM, CommonJS, and declaration files while the root import remains backward compatible.
client.soroban.simulate(transactionXdr) // → SorokitResult<SimulateTransactionResult>
client.soroban.prepare(params) // → SorokitResult<PreparedContractCall>
client.soroban.execute(signedXdr) // → SorokitResult<string> (tx hash)
client.soroban.read(params) // → SorokitResult<ContractCallResult>
// Contract interaction builder (new)
const builder = new ContractInteractionBuilder(client, contractId);
builder
.method('transfer')
.arg('to', destination)
.arg('amount', amount)
.build();
// Contract state tracking (new)
client.soroban.trackContractState(contractId, keys); // → AsyncGenerator
// Decode contract events (new)
client.soroban.decodeContractEvent(event); // → DecodedEvent | null
// Get contract call identity (new)
client.soroban.getCallIdentity(contractId); // → SorokitResult<ContractIdentity>
// Parse contract results (new)
client.soroban.parseContractResult(result); // → SorokitResult<ParsedResult>
// Simulate before execution (new)
client.soroban.simulator.simulate(transaction); // → SorokitResult<SimulationResult>
// Get factory statistics (new)
client.soroban.getFactoryStatistics(factoryId); // → SorokitResult<FactoryStats>
// Full invoke pipeline: prepare → sign → execute in one call
client.soroban.invoke(params, (xdr) =>
adapter.signTransaction({ transactionXdr: xdr, ... })
)buildContractDeploy validates its configuration before any network call, so a
missing endpoint or a malformed deployer address fails immediately with an
INVALID_CONFIG error naming every offending field and how to fix it. Call the
same check directly from a deployment script to fail before you spend a build:
import { validateDeployConfig, collectDeployConfigIssues } from "sorokit-core";
const check = validateDeployConfig({
rpcUrl,
horizonUrl,
networkConfig,
deployer,
});
if (check.status === "error") {
console.error(check.error.message);
// Deployment configuration is invalid — 2 problems found:
// 1. rpcUrl — rpcUrl is missing. Fix: Set rpcUrl to the Soroban RPC endpoint …
// 2. deployer — deployer is not a valid Stellar public key: "GNOPE". Fix: …
process.exit(1);
}
// Or render the issues yourself — each has { field, reason, hint }
const issues = collectDeployConfigIssues({
rpcUrl,
horizonUrl,
networkConfig,
deployer,
});Every function in sorokit-core returns a SorokitResult<T>. Nothing throws. Nothing rejects silently.
type SorokitResult<T> =
| { status: "ok"; data: T; error: null }
| { status: "error"; data: null; error: SorokitError };Usage:
const result = await client.account.get(publicKey);
if (result.status === "ok") {
console.log(result.data.balances);
} else {
console.error(result.error.code, result.error.message);
}This pattern means you handle errors where they happen, without wrapping everything in try/catch blocks or risking unhandled rejections propagating through your application.
Four adapters ship with sorokit-core. All require a Stellar Wallets Kit instance initialised separately:
import {
FreighterAdapter,
XBullAdapter,
LobstrAdapter,
HanaAdapter,
SWKSignAdapter,
} from "sorokit-core";
const adapter = new FreighterAdapter(swkInstance);
const adapter = new XBullAdapter(swkInstance);
const adapter = new LobstrAdapter(swkInstance);
const adapter = new HanaAdapter(swkInstance); // NEW
const adapter = new SWKSignAdapter(swkInstance); // NEWNew Features:
HanaAdapter- Support for Hana wallet integrationSWKSignAdapter- Custom SWK-based signing adaptersignTransactionOffline()- Sign transactions without wallet connection (NEW)walletStatusTracker- Real-time wallet connection monitoring (NEW)
Pass the adapter to client.wallet.connect() and client.wallet.signTransaction(). The adapter is the only stateful object in the system — the client itself remains stateless.
The SDK now includes circuit breaker and dynamic network switching:
// Circuit breaker for resilience (automatic)
const client = createSorokitClient({
network: "testnet",
enableCircuitBreaker: true, // Retries failed requests intelligently
});
// Dynamic network switching (new)
client.network.switchNetwork("mainnet"); // Switch at runtime
// Network resolution (new)
const resolved = await client.network.resolveNetwork("testnet");
if (resolved.status === "ok") {
console.log(resolved.data.horizonUrl, resolved.data.rpcUrl);
}Features:
circuitBreaker- Automatic retry logic with exponential backoffnetworkSwitcher- Switch between networks without recreating clientresolveNetwork- Programmatic network endpoint resolution
Account and transaction streams use async generators and poll Horizon at a configurable interval. Use an AbortController to stop a stream at any point:
const ac = new AbortController();
for await (const result of client.account.stream(
publicKey,
{ intervalMs: 3000 },
ac.signal,
)) {
if (result.status === "ok") {
// handle state update
}
}
// Stop the stream from anywhere
ac.abort();The same pattern applies to client.transaction.stream().
Transaction streams can use Horizon Server-Sent Events to avoid polling latency.
The sse and auto transports fall back to polling when the endpoint is
unavailable or closes before emitting an event:
for await (const result of client.transaction.stream(publicKey, {
transport: "auto",
})) {
if (result.status === "ok") console.log(result.data.transactions);
}Configure a bounded per-origin pool before creating Horizon or Soroban servers. Default behavior is unchanged when no pool is configured:
import { createConnectionPool, setConnectionPool } from "sorokit-core";
setConnectionPool(createConnectionPool({
maxPoolSize: 10,
keepAliveTimeoutMs: 30_000,
socketTimeoutMs: 30_000,
}));Use Counter for event counts and Timer for operation latency. Timers write to
the existing bounded metrics collector and can be exported with
getPerformanceMetrics() or exportPerformanceMetrics():
const cacheHits = new Counter("cache.hit");
cacheHits.increment();
const timer = startTimer("transaction.submit");
try {
await submit();
timer.stop(true);
} catch (error) {
timer.stop(false);
throw error;
}Applications that only need one subsystem can use the tree-shakeable
sorokit-core/wallet and sorokit-core/account entry points.
// Preset networks
createSorokitClient({ network: "mainnet" });
createSorokitClient({ network: "testnet" });
createSorokitClient({ network: "futurenet" });
// Override Horizon or RPC URLs for self-hosted infrastructure
createSorokitClient({
network: "mainnet",
horizonUrl: "https://my-horizon.example.com",
rpcUrl: "https://my-rpc.example.com",
});A mock client is provided for writing tests without hitting real network endpoints:
import {
createMockClient,
createMockWalletAdapter,
} from "sorokit-core/testing";
const client = createMockClient();
// Every method is a vi.fn() stub — override per test
client.account.get.mockResolvedValueOnce(
ok({ publicKey: "G...", balances: [] }),
);
// Mock wallet adapter for signing flows
const adapter = createMockWalletAdapter();Requires
vitestas a peer dependency.
| Example | Shows |
|---|---|
examples/router-swap |
DEX swap: path discovery, quote, sign, submit, track |
examples/react-wallet-connect |
React: connect wallet, fetch balances, build + sign + submit payment |
examples/vue-soroban |
Vue 3: contract method selection, invoke with progress, account streaming |
examples/next-serverless |
Next.js + Lambda: server-side signing, batch account ops, Vercel/AWS handler |
Examples are type-checked against the SDK source with npm run typecheck:examples.
For a side-by-side comparison of stellar-sdk patterns vs sorokit-core, see docs/migration-guide.md.
Stateless — no internal state, no singleton, no side effects beyond network calls. Create as many clients as you need.
No-throw — every function returns SorokitResult<T>. Errors are values, not exceptions.
Framework-agnostic — zero dependency on React, Vue, or any UI framework. Works in Node, the browser, and server-side rendering environments.
Adapter-based wallets — wallet integration is delegated to Stellar Wallets Kit, keeping sorokit-core decoupled from wallet implementation details.
| Example | Shows |
|---|---|
examples/router-swap |
Frontend router integration: quote → swap → transaction tracking, with wallet signing and router error handling |
Examples are type-checked against the SDK source with npm run typecheck:examples.
- ✅ Circuit Breaker - Automatic resilience for failed network requests
- ✅ Dynamic Network Switching - Change networks without recreating the client
- ✅ Transaction Simulation - Test transactions before sending (simulator module)
- ✅ Contract Interaction Builder - Fluent API for complex contract calls
- ✅ Event Decoding - Programmatic contract event parsing
- ✅ Deploy Validation - Pre-flight validation for contract deployments with actionable error messages
- ✅ Offline Signing - Sign transactions without wallet connection
- ✅ Multi-Signature Support - Build and manage multi-sig transactions
- ✅ Path Payments - Advanced path payment calculations
- ✅ Transaction History - Export and query historical transactions with flexible formats
- ✅ Wallet Adapters - Hana and SWK signing adapters for extended wallet support
- ✅ Balance Alerts - Monitor account balance changes in real-time
- ✅ Activity Summaries - Aggregate account activity metrics and analytics
- ✅ Key Rotation - Rotate account keys securely
- ✅ Sponsorship - Manage account sponsorships
- ✅ Distributed Tracing - Debug transactions and calls with detailed tracing
- ✅ Advanced Logging - Structured logging with multiple levels and configurable output
- ✅ Configuration Management - Centralized config validation with per-field guidance
- 30+ new test files with comprehensive coverage
- Property-based testing for transaction building
- Router integration tests with real-world scenarios
- Network resilience and circuit breaker tests
- Contract deployment validation tests
- Bundle size tracking with 50 KB gzipped budget
- Optimized network calls with circuit breaker
- Efficient contract state tracking
- Reduced memory footprint with streaming
Pull requests are welcome. For significant changes, please open an issue first to discuss what you'd like to change.
The docs/recipes/ directory contains 15 practical, copy-paste-ready recipes for common Stellar and Soroban patterns. Each recipe includes a problem statement, working code, and testing tips.
| # | Recipe | Use case |
|---|---|---|
| 1 | Multi-sig approval workflow | N-of-M signatures for high-value payments |
| 2 | Escrow with timelock | Funds held until a future timestamp |
| 3 | DEX atomic swap | Swap two assets atomically on the Stellar DEX |
| 4 | Soroban contract invoke with error handling | Invoke a smart contract with full error recovery |
| 5 | Batch payment with progress tracking | Send many payments, track success/failure per item |
| 6 | Portfolio rebalancing | Read balances and swap to hit target allocations |
| 7 | Offer management on DEX | Create, update, and cancel limit orders |
| 8 | Account key rotation | Replace a compromised signing key securely |
| 9 | Payment with memo (SEP-7 style) | Attach a memo for exchange routing or identification |
| 10 | Account recovery workflow | Recover account access via designated guardians |
| 11 | Trustline management | Add, audit, and remove asset trustlines |
| 12 | Path payment (cross-asset) | Send one asset, recipient receives a different asset |
| 13 | Real-time balance alerts and streaming | React to balance changes without polling yourself |
| 14 | Contract deployment with validation | Deploy a Soroban WASM with pre-flight checks |
| 15 | Fee estimation and surge pricing | Estimate fees accurately before building a transaction |
The documentation now includes task-oriented, executable workflows and a contributor-facing architecture guide:
| Guide | Use it for |
|---|---|
docs/workflows.md |
Complete transaction lifecycle, wallet signing, multisignature signing, Soroban calls, trustline approval, cost planning, refunds, and recovery patterns |
docs/architecture.md |
Module boundaries, data flow, result/error conventions, extension guidance, and migration from direct Stellar SDK usage |
Both guides use the current exported API shape and keep policy, construction, signing, submission, and recovery concerns separate.
API servers can expose getFactoryStatistics at a route such as
GET /factory/:id/statistics. Supply an adapter that reads the factory pair
count and deployment metadata; the function returns Sorokit's standard
structured JSON result.
import { decodeContractEvent, queryContractEvents } from "sorokit-core";
const events = await queryContractEvents(factoryId, undefined, { horizonUrl });
for (const event of events) {
const decoded = decodeContractEvent(event);
if (decoded?.type === "factory.pair_created") {
console.log(decoded.data);
}
}Pass custom decoders as the second argument to support application-specific events. Custom decoders run first, so adding new built-in event types remains backward-compatible.
Mainnet submissions require explicit confirmation when native XLM exposure exceeds
1,000 XLM (configurable). Both client.transaction.submit and its
submitTransaction alias return MAINNET_SAFETY_LIMIT before contacting Horizon
unless bypassMainnetSafety is exactly true:
const result = await client.transaction.submitTransaction(signedXdr, {
mainnetSafetyThresholdXlm: 1000,
bypassMainnetSafety: true, // Set only after reviewing the transaction.
});Warnings include the native amount, threshold, source account, operation count, and operation types whose exposure cannot be determined from XDR. Account merges, balance claims, liquidity pools, and contract calls require confirmation even when no large amount is visible in the envelope. Native offers and payment spend limits are counted with exact stroop arithmetic. Fees, reserve changes, and non-native asset valuations are outside this guard. Other networks are unaffected.
Soroban submissions use the same guard. Pass safety options as the fourth argument
to client.soroban.execute, or the fifth to client.soroban.invoke:
await client.soroban.execute(signedXdr, undefined, undefined, {
bypassMainnetSafety: true,
});