stellar-contracts-kit
v0.1.1
Published
TypeScript SDK for Soroban smart contract interactions on Stellar
Downloads
138
Maintainers
Readme
stellar-contracts-kit
TypeScript SDK for Soroban smart contracts on Stellar. Handles wallet connection, typed contract clients, and a CLI that generates TypeScript interfaces directly from any live on-chain contract.
Features
- CLI code generator:
npx sckgenerates fully-typed TypeScript or JavaScript interfaces from any live contract spec - Interactive CLI: arrow-key command picker, input validation, network selection
- Built-in wallet modal: auto-detects Freighter, Cyphras, and Lobstr, opens a picker UI when no wallet is specified
- Three call modes: auto, read-only, and force-invoke on every contract method
- Full type coverage: structs, enums, unions, tuples,
Uint8Array,bigint,Option,Vec,Map - Contract spec caching: spec fetched once per contract ID, subsequent calls are instant
- Contract restore: one-call recovery when a contract's on-chain state has expired
- Works with any frontend framework (React, Vue, Svelte, vanilla JS)
Installation
npm install stellar-contracts-kitCLI
Interactive mode
npx sckLaunches an arrow-key menu to pick a command, then guides you through the rest.
Generate types
Fetch a live contract spec and generate a typed TypeScript interface + example file:
npx sck generate --contract CABC... --network testnetOr add it as an npm script so the team can run it without npx:
"scripts": {
"generate": "sck generate --contract CABC... --network testnet"
}npm run generateWith options:
npx sck generate \
--contract CABC... \
--network testnet \
--name CounterContract \
--out src/contracts/counter.tsFor JavaScript output:
npx sck generate --contract CABC... --network testnet --jsOutput files:
| File | Description |
|------|-------------|
| contracts/counter.ts | TypeScript interface + custom types + contract ID |
| contracts/counter.example.ts | Ready-to-adapt usage example for every method |
Generated contracts/counter.ts:
import type { ContractMethodFn } from 'stellar-contracts-kit'
export interface CounterContract {
get: ContractMethodFn<number, []>
increment: ContractMethodFn<number, []>
reset: ContractMethodFn<void, []>
}
export const CONTRACT_ID = 'CABC...' as constRe-run npx sck generate after a contract upgrade to refresh the types.
Inspect a contract
Print all functions and custom types for any contract directly in the terminal, without generating files:
npx sck inspect --contract CABC... --network testnetOutput:
Contract : CABC...
Network : testnet
Functions (5)
get() -> number
increment() -> number
reset() -> void
transfer(from: string, to: string, amount: bigint) -> void
balance(account: string) -> bigint
Custom Types (2)
struct TokenInfo { symbol: string, decimals: number }
enum Status { Active = 0, Inactive = 1 }Custom network
npx sck generate \
--contract CABC... \
--rpc-url https://my-rpc.example.com \
--passphrase "My Custom Network"CLI options
Commands:
npx sck generate [options] Generate TypeScript types and example file
npx sck inspect [options] Print contract interface to the terminal
npx sck Interactive mode (arrow-key selection)
Shared options:
--contract Contract address (C..., 56 chars) [required]
--network testnet | mainnet | futurenet
--rpc-url Custom RPC URL
--passphrase Custom network passphrase
Generate-only options:
--out Output file path (default: ./contracts/<name>.ts)
--name Interface name (default: derived from --out)
--alias tsconfig path alias (auto-detected from tsconfig.json)
--js JavaScript output instead of TypeScript
--help, -h Show helpSDK
Quick start
import { StellarContractsKit } from 'stellar-contracts-kit'
const kit = new StellarContractsKit({ network: 'testnet' })
// Opens built-in wallet picker modal
const { address } = await kit.connect()
// Load a contract client
const counter = await kit.contract('CABC...')
// Read-only (no wallet, no TX)
const { result } = await counter.get.read()
// Write (requires connected wallet)
const { txHash } = await counter.increment.invoke()With generated types
import type { CounterContract } from './contracts/counter.js'
import { CONTRACT_ID } from './contracts/counter.js'
const counter = await kit.contract<CounterContract>(CONTRACT_ID)
// Full IDE autocomplete + type checking on all methods
const { result } = await counter.get.read()
const { txHash } = await counter.increment.invoke()Wallet Connection
Built-in modal (recommended)
Calling connect() with no wallet configured opens a modal that auto-detects installed wallets:
const kit = new StellarContractsKit({ network: 'testnet' })
const { address } = await kit.connect()The modal shows all supported wallets in order, with "Install" links for wallets that are not detected.
Specific wallet adapter
import { StellarContractsKit, FreighterAdapter } from 'stellar-contracts-kit'
const kit = new StellarContractsKit({
network: 'testnet',
wallet: new FreighterAdapter(),
})
const { address } = await kit.connect()Supported wallets
| Wallet | Adapter | Install |
|--------|---------|---------|
| Freighter | FreighterAdapter | freighter.app |
| Cyphras | CyphrasAdapter | cyphras.com |
| Lobstr | LobstrAdapter | lobstr.co |
Other wallet methods
await kit.disconnect()
kit.isConnected() // boolean
await kit.getAddress() // string, throws WALLET_NOT_CONNECTED if none
kit.getWallet() // WalletAdapter | null
kit.setWallet(adapter) // replace active walletContract Interaction
Call modes
Every method on the contract client has three modes:
// Auto: simulates first, submits TX only if auth is required
const result = await counter.increment()
// Read: simulate only, no wallet needed, no TX submitted
const { result } = await counter.get.read()
// Invoke: always submits a TX, requires a connected wallet
const { txHash, result } = await counter.increment.invoke()Passing arguments
Arguments are passed positionally in the order defined by the contract:
const { result } = await token.balance.read(userAddress)
const { txHash } = await token.transfer.invoke(from, to, amount)Spec caching
The contract spec is fetched once per contract ID and cached in memory. Subsequent kit.contract() calls with the same ID return instantly:
const counter = await kit.contract<CounterContract>(CONTRACT_ID) // fetches spec
const counter2 = await kit.contract<CounterContract>(CONTRACT_ID) // instant
kit.clearSpecCache(CONTRACT_ID) // clear one
kit.clearSpecCache() // clear allCustom Networks
const kit = new StellarContractsKit({
network: {
rpcUrl: 'https://my-soroban-rpc.example.com',
networkPassphrase: 'My Custom Network',
horizonUrl: 'https://my-horizon.example.com',
},
})Built-in network presets:
import { NETWORKS } from 'stellar-contracts-kit'
NETWORKS.testnet // Test SDF Network
NETWORKS.mainnet // Public Global Stellar Network
NETWORKS.futurenet // Test SDF Future NetworkError Handling
All errors thrown by the kit are StellarContractError instances with a code property:
import { isContractKitError } from 'stellar-contracts-kit'
try {
await counter.increment.invoke()
} catch (err) {
if (isContractKitError(err)) {
switch (err.code) {
case 'WALLET_REJECTED':
console.log('User rejected the transaction.')
break
case 'CONTRACT_RESTORE_REQUIRED':
await kit.restoreContract(CONTRACT_ID)
await counter.increment.invoke() // retry
break
case 'TX_FAILED':
console.error('On-chain failure:', err.message)
break
}
}
}Error codes
| Code | Description |
|------|-------------|
| WALLET_NOT_FOUND | Wallet extension not installed |
| WALLET_NOT_CONNECTED | No wallet is connected |
| WALLET_REJECTED | User rejected the connection or signing request |
| WALLET_NETWORK_MISMATCH | Wallet is on a different network than the kit |
| CONTRACT_NOT_FOUND | Contract does not exist on the network |
| CONTRACT_SPEC_ERROR | Could not parse the contract WASM spec |
| CONTRACT_SIMULATION_FAILED | Transaction simulation failed |
| CONTRACT_RESTORE_REQUIRED | Contract state expired, call restoreContract() |
| INVALID_CONTRACT_ID | Invalid contract address format |
| INVALID_PARAMS | Wrong number of arguments passed to a method |
| TX_SUBMISSION_FAILED | Transaction rejected at submission |
| TX_FAILED | Transaction accepted but failed on-chain |
| TX_TIMEOUT | Transaction not confirmed within the polling window |
| RPC_ERROR | RPC call failed or returned an unexpected response |
| UNKNOWN | Unexpected error |
Restoring Expired Contracts
Soroban contracts can expire when their TTL runs out. Any call that touches expired state throws CONTRACT_RESTORE_REQUIRED. Use restoreContract() to recover:
try {
await counter.increment.invoke()
} catch (err) {
if (isContractKitError(err) && err.code === 'CONTRACT_RESTORE_REQUIRED') {
const { txHash } = await kit.restoreContract(CONTRACT_ID)
console.log('Restored:', txHash)
await counter.increment.invoke() // retry
}
}Custom Wallet Adapter
Implement WalletAdapter to add support for any wallet:
import type { WalletAdapter } from 'stellar-contracts-kit'
class MyWalletAdapter implements WalletAdapter {
readonly name = 'MyWallet'
readonly installUrl = 'https://mywallet.example.com'
isAvailable(): boolean | Promise<boolean> { ... }
async connect(): Promise<{ address: string }> { ... }
async disconnect(): Promise<void> { ... }
async getAddress(): Promise<string> { ... }
async getNetworkPassphrase(): Promise<string> { ... }
async signTransaction(xdr: string, opts): Promise<string> { ... }
async signAuthEntry(entryXdr: string, opts): Promise<string> { ... }
}API Reference
StellarContractsKit
new StellarContractsKit(options: StellarContractsKitOptions)| Method | Returns | Description |
|--------|---------|-------------|
| connect() | Promise<{ address: string }> | Connect to wallet or open picker modal |
| disconnect() | Promise<void> | Disconnect and clear the active wallet |
| isConnected() | boolean | True if a wallet is active |
| getAddress() | Promise<string> | Address of the connected wallet |
| getWallet() | WalletAdapter \| null | Active wallet adapter |
| setWallet(adapter) | void | Set or replace the active wallet |
| getNetwork() | NetworkConfig | Current network configuration |
| contract<T>(id) | Promise<T> | Load a typed contract client |
| restoreContract(id) | Promise<{ txHash: string }> | Restore expired contract state |
| clearSpecCache(id?) | void | Clear cached spec for one or all contracts |
ContractMethodFn<TReturn, TArgs>
The type for each method on a generated contract interface:
ContractMethodFn<TReturn, TArgs extends unknown[] = unknown[]>| Type param | Description |
|------------|-------------|
| TReturn | Return value type (number, bigint, string, void, custom struct, ...) |
| TArgs | Labeled tuple of argument types ([from: string, amount: bigint]) |
