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
- 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 |
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);
}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().
// 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.
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.
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.