TypeScript SDK for deploying and managing OpenZeppelin Smart Account contracts on Stellar/Soroban with WebAuthn passkey authentication.
- Passkey Authentication: Create and manage smart wallets secured by WebAuthn passkeys
- Session Management: Automatic session persistence for seamless reconnection
- Multiple Signer Types: Support for passkeys (secp256r1), Ed25519 keys, and policy signers
- Context Rules: Fine-grained authorization control for different operations
- Policy Support: Threshold multisig, spending limits, and custom policies
- Storage Adapters: Flexible credential storage (IndexedDB, localStorage, custom)
pnpm add smart-account-kitimport { SmartAccountKit, IndexedDBStorage } from 'smart-account-kit';
// Initialize the SDK
const kit = new SmartAccountKit({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: 'Test SDF Network ; September 2015',
accountWasmHash: 'YOUR_ACCOUNT_WASM_HASH',
webauthnVerifierAddress: 'CWEBAUTHN_VERIFIER_ADDRESS',
storage: new IndexedDBStorage(),
});
// On page load - silent restore from stored session
const result = await kit.connectWallet();
if (!result) {
// No stored session, show connect button
showConnectButton();
}
// User clicks "Create Wallet"
const { contractId, credentialId } = await kit.createWallet('My App', 'user@example.com', {
autoSubmit: true,
});
// User clicks "Connect Wallet" - prompts for passkey selection
await kit.connectWallet({ prompt: true });
// Sign and submit a transaction
const result = await kit.signAndSubmit(transaction);| Option | Type | Required | Description |
|---|---|---|---|
rpcUrl |
string | Yes | Stellar RPC URL |
networkPassphrase |
string | Yes | Network passphrase |
accountWasmHash |
string | Yes | Smart account WASM hash |
webauthnVerifierAddress |
string | Yes | WebAuthn verifier contract address |
timeoutInSeconds |
number | No | Transaction timeout (default: 30) |
storage |
StorageAdapter | No | Credential storage adapter |
rpId |
string | No | WebAuthn relying party ID |
rpName |
string | No | WebAuthn relying party name |
relayerUrl |
string | No | Relayer proxy URL for fee sponsoring |
Configure a relayer URL to enable gasless transactions. The SDK posts { func, auth } for
invokeHostFunction flows and { xdr } for signed transactions (e.g., deployments).
const kit = new SmartAccountKit({
// ... other config
relayerUrl: 'https://my-relayer-proxy.example.com',
});
// Transactions automatically use the Relayer if configured
await kit.transfer(tokenContract, recipient, amount);
// To bypass the Relayer for specific operations
await kit.transfer(tokenContract, recipient, amount, { forceMethod: 'rpc' });import {
IndexedDBStorage, // Recommended for web apps
LocalStorageAdapter, // Simple fallback
MemoryStorage, // For testing
} from 'smart-account-kit';
// Use IndexedDB (recommended)
const storage = new IndexedDBStorage();
// Or implement your own
class MyStorage implements StorageAdapter {
async save(credential: StoredCredential): Promise<void> { ... }
async get(credentialId: string): Promise<StoredCredential | null> { ... }
async saveSession(session: StoredSession): Promise<void> { ... }
async getSession(): Promise<StoredSession | null> { ... }
// ... other methods
}The main SDK client class.
import { SmartAccountKit } from 'smart-account-kit';| Method | Description |
|---|---|
constructor(config: SmartAccountConfig) |
Initialize SDK with configuration |
createWallet(appName, userName, options?) |
Create new smart wallet with passkey |
connectWallet(options?) |
Connect to existing wallet |
disconnect() |
Disconnect and clear session |
authenticatePasskey() |
Authenticate with passkey without connecting |
discoverContractsByCredential(credentialId) |
Find contracts by credential ID via indexer |
discoverContractsByAddress(address) |
Find contracts by G/C-address via indexer |
sign(transaction, options?) |
Sign auth entries (use signAndSubmit instead) |
signAndSubmit(transaction, options?) |
Sign, re-simulate, and submit (recommended) |
signAuthEntry(authEntry, options?) |
Sign a single auth entry |
execute(target, targetFn, targetArgs) |
Build a smart-account mediated contract call |
executeAndSubmit(target, targetFn, targetArgs, options?) |
Build, sign, and submit a smart-account mediated contract call |
fundWallet(nativeTokenContract) |
Fund via Friendbot (testnet) |
transfer(tokenContract, recipient, amount) |
Direct token transfer |
getContractDetailsFromIndexer(contractId) |
Get contract details from indexer |
convertPolicyParams(params) |
Convert policy params to ScVal |
buildPoliciesScVal(policies) |
Build policies ScVal for context rules |
createWallet() creates and deploys a new smart account tied to a freshly generated passkey. connectWallet() restores or prompts into an existing wallet, authenticatePasskey() gives you passkey auth without connecting, and disconnect() only clears session state.
For transactions, signAndSubmit() is the default for smart-account auth flows, executeAndSubmit() is the preferred path for arbitrary smart-account mediated contract calls, and sign() / signAuthEntry() remain available when you need to inspect or compose around signed auth entries directly.
| Property | Type | Description |
|---|---|---|
kit.signers |
SignerManager |
Manage signers on rules |
kit.rules |
ContextRuleManager |
CRUD for context rules |
kit.policies |
PolicyManager |
Manage policies on rules |
kit.credentials |
CredentialManager |
Credential lifecycle |
kit.multiSigners |
MultiSignerManager |
Multi-signer flows |
kit.externalSigners |
ExternalSignerManager |
G-address signers |
kit.indexer |
IndexerClient | null |
Indexer client for contract discovery |
kit.events |
SmartAccountEventEmitter |
Event subscription |
// Create a new wallet
const { contractId, credentialId } = await kit.createWallet('My App', 'user@example.com', {
autoSubmit: true, // Automatically deploy the wallet
autoFund: true, // Fund via Friendbot (testnet only)
nativeTokenContract: 'CDLZFC3...',
});
// Connect to existing wallet
const result = await kit.connectWallet(); // Silent restore from session
await kit.connectWallet({ prompt: true }); // Prompt user to select passkey
await kit.connectWallet({ fresh: true }); // Ignore session, always prompt
await kit.connectWallet({ credentialId: '...' }); // Connect with specific credential
await kit.connectWallet({ contractId: 'C...' }); // Connect with specific contract
// Transfer tokens
const result = await kit.transfer('CTOKEN...', 'GRECIPIENT...', 100);
// Build an arbitrary smart-account mediated call
const tx = await kit.execute('CTARGET...', 'set_config', [owner, threshold]);
// Or build + sign + submit in one step
const execResult = await kit.executeAndSubmit('CTARGET...', 'set_config', [owner, threshold]);
// Disconnect
await kit.disconnect();The generated contract client remains available as kit.wallet after connection. Use it when you need exact contract parity or one of the raw methods that the SDK intentionally does not wrap: upgrade, batch_add_signer, get_signer_id, get_policy_id, and get_context_rules_count.
The default rule is simple: if the SDK adds orchestration, session handling, signer resolution, or submission logic, keep the wrapper. If the method is just a thin contract call, use kit.wallet directly.
Manage signers on context rules.
| Method | Description |
|---|---|
addPasskey(contextRuleId, appName, userName, options?) |
Add passkey signer |
addDelegated(contextRuleId, address) |
Add G-address signer |
remove(contextRuleId, signer) |
Remove a signer |
// Add a new passkey signer
const { credentialId, transaction } = await kit.signers.addPasskey(
0, // Context rule ID
'My App', // App name
'Recovery Key', // User name
{ nickname: 'Backup YubiKey' }
);
// Add a delegated signer (Stellar account)
await kit.signers.addDelegated(0, 'GABC...');
// Remove a signer by value; the SDK resolves signer IDs internally
await kit.signers.remove(0, signer);batch_add_signer stays on the raw wallet client because the SDK does not add enough ergonomics or cross-cutting behavior to justify another wrapper.
Manage context rules.
| Method | Description |
|---|---|
add(contextType, name, signers, policies) |
Create rule |
get(contextRuleId) |
Get single rule |
list() |
List all active rules via indexer-backed rule discovery |
getAll(contextRuleType) |
Get active rules of a type via indexer-backed rule discovery |
remove(contextRuleId) |
Delete rule |
updateName(contextRuleId, name) |
Update rule name |
updateExpiration(contextRuleId, ledger) |
Update expiration ledger |
// Add a new context rule
await kit.rules.add(contextType, 'Rule Name', signers, policies);
// Read a specific rule directly from chain
const rule = (await kit.rules.get(0)).result;
// Active rule discovery requires indexer access
const rules = await kit.rules.list();
const allRules = await kit.rules.getAll(contextType);kit.rules.get() reads a specific rule directly from the contract. kit.rules.list() and kit.rules.getAll() are indexer-backed by design because the contract exposes get_context_rule(id) and get_context_rules_count(), but not an iterator over active rule IDs after deletions.
// Update rules
await kit.rules.updateName(0, 'New Name');
await kit.rules.updateExpiration(0, expirationLedger);
// Remove a rule
await kit.rules.remove(0);Manage policies on context rules.
| Method | Description |
|---|---|
add(contextRuleId, policyAddress, installParams) |
Add policy to rule |
remove(contextRuleId, policyAddress) |
Remove policy from rule |
// Add a policy
await kit.policies.add(0, 'CPOLICY...', installParams);
// Remove a policy
await kit.policies.remove(0, 'CPOLICY...');Manage stored credentials.
| Method | Description |
|---|---|
getAll() |
Get all stored credentials |
getForWallet() |
Get credentials for current wallet |
getPending() |
Get pending deployments |
create(options?) |
Create new credential |
save(credential) |
Save credential to storage |
deploy(credentialId, options?) |
Deploy pending credential |
sync(credentialId) |
Sync with on-chain state |
syncAll() |
Sync all credentials |
delete(credentialId) |
Delete credential |
// Get credentials
const all = await kit.credentials.getAll();
const pending = await kit.credentials.getPending();
const walletCreds = await kit.credentials.getForWallet();
// Deploy a pending credential
const result = await kit.credentials.deploy('credential-id', { autoSubmit: true });
// Sync with on-chain state
await kit.credentials.syncAll();
// Delete a pending credential
await kit.credentials.delete('credential-id');Credential lifecycle notes: create() and save() create local pending credentials, deploy() moves a credential into a connected wallet flow, sync() and syncAll() reconcile local pending state against on-chain deployment state, and delete() is for pending credentials that never deployed.
Multi-signer transaction flows.
| Method | Description |
|---|---|
transfer(tokenContract, recipient, amount, selectedSigners) |
Multi-sig transfer |
getAvailableSigners() |
Get all signers from active rules via indexer-backed rule discovery |
needsMultiSigner(signers) |
Check if multi-sig is needed |
buildSelectedSigners(signers, activeCredentialId?) |
Build signer selection |
operation(assembledTx, selectedSigners, options?) |
Execute generic multi-sig operation |
// Get all available signers (requires indexer access)
const signers = await kit.multiSigners.getAvailableSigners();
// Check if multi-sig is needed
if (kit.multiSigners.needsMultiSigner(signers)) {
// Build selected signers for transaction
const selected = kit.multiSigners.buildSelectedSigners(signers, activeCredentialId);
// Execute multi-sig transfer
const result = await kit.multiSigners.transfer(
'CTOKEN...',
'GRECIPIENT...',
'100',
selected
);
}Multi-signer guidance: use buildSelectedSigners() to assemble the signer set, then pass resolveContextRuleIds when a transaction can match more than one rule or when you want to pin the auth context explicitly. getAvailableSigners() is indexer-backed for the same reason as kit.rules.list().
import { ExternalSignerManager } from 'smart-account-kit';Manage G-address (delegated) signers.
| Method | Description |
|---|---|
addFromSecret(secretKey) |
Add keypair signer (memory only) |
addFromWallet(adapter) |
Connect external wallet |
restoreConnections() |
Restore persisted wallet connections |
canSignFor(address) |
Check if can sign for address |
signAuthEntry(address, authEntry) |
Sign auth entry for address |
getAll() |
List all external signers |
remove(address) |
Remove signer |
// Add a keypair signer
kit.externalSigners.addFromSecret('SXXX...');
// Connect external wallet
await kit.externalSigners.addFromWallet(walletAdapter);
// Check signing capability
if (kit.externalSigners.canSignFor('GABC...')) {
const signedEntry = await kit.externalSigners.signAuthEntry('GABC...', authEntry);
}import type {
SmartAccountConfig, // SDK initialization config
PolicyConfig, // Policy contract config
SubmissionOptions, // Transaction submission options
} from 'smart-account-kit';import type {
StoredCredential, // Full credential metadata
StoredSession, // Auto-reconnect session data
CredentialDeploymentStatus, // "pending" | "failed"
StorageAdapter, // Storage backend interface
} from 'smart-account-kit';import type {
CreateWalletResult, // Wallet creation outcome
ConnectWalletResult, // Connection outcome
TransactionResult, // Transaction outcome (success/failure)
} from 'smart-account-kit';import type {
ExternalWalletAdapter, // Interface for wallet extensions
ConnectedWallet, // Single wallet connection info
SelectedSigner, // Signer selection for multi-sig
} from 'smart-account-kit';import type {
ContractSigner, // On-chain signer type (alias: Signer)
ContextRule, // Context rule structure
ContextRuleType, // Rule type enum
AuthPayload, // Smart-account auth payload
WebAuthnSigData, // WebAuthn signature format
SimpleThresholdAccountParams, // M-of-N multisig params
WeightedThresholdAccountParams, // Weighted voting params
SpendingLimitAccountParams, // Time-limited spending params
} from 'smart-account-kit';import {
createDelegatedSigner, // Create Stellar account signer (G-address)
createExternalSigner, // Create custom verifier signer
createWebAuthnSigner, // Create passkey signer
createEd25519Signer, // Create Ed25519 signer
} from 'smart-account-kit';
// Create a delegated signer
const signer = createDelegatedSigner('GABC...', 'ed25519-verifier-address');
// Create a WebAuthn signer
const passkeySigner = createWebAuthnSigner(verifierAddress, publicKey, credentialId);import {
createDefaultContext, // Default rule (matches any operation)
createCallContractContext, // Rule for specific contract calls
createCreateContractContext, // Rule for contract deployments
} from 'smart-account-kit';
// Create a context for calling a specific contract
const context = createCallContractContext('CCONTRACT...');import {
createThresholdParams, // M-of-N multisig
createWeightedThresholdParams, // Weighted voting
createSpendingLimitParams, // Time-limited spending
LEDGERS_PER_HOUR, // ~720 ledgers
LEDGERS_PER_DAY, // ~17,280 ledgers
LEDGERS_PER_WEEK, // ~120,960 ledgers
} from 'smart-account-kit';
// Create 2-of-3 multisig params
const thresholdParams = createThresholdParams(2);
// Create spending limit params
const spendingParams = createSpendingLimitParams(
'CTOKEN...', // Token contract
BigInt(1000 * 10_000_000), // 1000 tokens in stroops
LEDGERS_PER_DAY // Reset period
);import {
getCredentialIdFromSigner, // Extract credential ID from signer
signersEqual, // Compare two signers
truncateAddress, // Display helper
formatSignerForDisplay, // Display helper
} from 'smart-account-kit';import {
WEBAUTHN_TIMEOUT_MS, // WebAuthn timeout (60000ms)
BASE_FEE, // Transaction base fee
STROOPS_PER_XLM, // 10,000,000
FRIENDBOT_RESERVE_XLM, // Friendbot reserve amount
} from 'smart-account-kit';import {
SmartAccountError, // Base error class
SmartAccountErrorCode, // Error codes enum
WalletNotConnectedError, // No wallet connected
CredentialNotFoundError, // Credential not found in storage
SignerNotFoundError, // Signer not found on-chain
SimulationError, // Transaction simulation failed
SubmissionError, // Transaction submission failed
ValidationError, // Input validation failed
WebAuthnError, // WebAuthn operation failed
SessionError, // Session management error
wrapError, // Error wrapper utility
} from 'smart-account-kit';
// Error handling
try {
await kit.transfer(...);
} catch (error) {
if (error instanceof WalletNotConnectedError) {
// Handle not connected
} else if (error instanceof SimulationError) {
// Handle simulation failure
}
}import { SmartAccountEventEmitter } from 'smart-account-kit';
import type { SmartAccountEventMap, SmartAccountEvent, EventListener } from 'smart-account-kit';| Event | Description |
|---|---|
walletConnected |
When connected to wallet |
walletDisconnected |
When disconnected |
credentialCreated |
When passkey registered |
credentialDeleted |
When credential removed |
sessionExpired |
When session expires |
transactionSigned |
When auth entries signed |
transactionSubmitted |
When tx submitted |
// Listen to events
kit.events.on('walletConnected', ({ contractId }) => {
console.log('Connected to:', contractId);
});
kit.events.on('transactionSubmitted', ({ hash, success }) => {
console.log('Transaction:', hash, success ? 'succeeded' : 'failed');
});
// One-time listener
kit.events.once('walletConnected', handler);
// Remove listener
kit.events.off('walletConnected', handler);import { StellarWalletsKitAdapter } from 'smart-account-kit';
import type { StellarWalletsKitAdapterConfig } from 'smart-account-kit';
// Create adapter for StellarWalletsKit integration
const adapter = new StellarWalletsKitAdapter({
kit: stellarWalletsKit,
onConnectionChange: (connected) => {
console.log('Wallet connection changed:', connected);
},
});
// Use with external signers
await kit.externalSigners.addFromWallet(adapter);The SDK includes a Relayer client for fee-sponsored transaction submission via the Relayer proxy.
import {
RelayerClient,
RelayerErrorCodes,
} from 'smart-account-kit';
import type {
RelayerResponse,
RelayerSendOptions,
RelayerErrorCode,
} from 'smart-account-kit';When the Relayer is configured in SmartAccountKit, it's used automatically for all transaction submissions:
const kit = new SmartAccountKit({
// ... other config
relayerUrl: 'https://my-relayer-proxy.example.com',
});
// Transactions automatically use the Relayer
await kit.transfer(tokenContract, recipient, amount);
// Bypass the Relayer for specific operations
await kit.transfer(tokenContract, recipient, amount, { forceMethod: 'rpc' });
// Access the Relayer client directly
if (kit.relayer) {
const result = await kit.relayer.sendXdr(signedTransaction);
}const relayer = new RelayerClient('https://my-relayer-proxy.example.com');
// Submit a transaction for fee sponsoring (func + auth)
const result = await relayer.send(funcXdr, authXdrs);
// Or submit a signed transaction for fee-bumping
const xdrResult = await relayer.sendXdr(signedTransaction);
if (result.success) {
console.log('Transaction hash:', result.hash);
} else {
console.error('Failed:', result.error, result.errorCode);
}The SDK includes an indexer client for reverse lookups from signer credentials to smart account contracts.
import {
IndexerClient,
IndexerError,
DEFAULT_INDEXER_URLS,
} from 'smart-account-kit';
import type {
IndexerConfig,
IndexedContractSummary,
IndexedSigner,
IndexedPolicy,
IndexedContextRule,
CredentialLookupResponse,
AddressLookupResponse,
ContractDetailsResponse,
IndexerStatsResponse,
} from 'smart-account-kit';// Indexer is auto-configured for testnet/mainnet
const kit = new SmartAccountKit({ /* config */ });
// Discover contracts by credential ID
const contracts = await kit.discoverContractsByCredential(credentialId);
// Discover contracts by address
const contracts = await kit.discoverContractsByAddress('GABC...');
// Get contract details
const details = await kit.getContractDetailsFromIndexer('CABC...');
// Or use the indexer client directly
if (kit.indexer) {
const stats = await kit.indexer.getStats();
const healthy = await kit.indexer.isHealthy();
}// Create client for a known Stellar network
const indexer = IndexerClient.forNetwork('Test SDF Network ; September 2015');
const mainnetIndexer = IndexerClient.forNetwork('Public Global Stellar Network ; September 2015');
// Or with a custom URL
const indexer = new IndexerClient({
baseUrl: 'https://smart-account-indexer.sdf-ecosystem.workers.dev',
timeout: 10000,
});
// Lookup by credential ID
const { contracts } = await indexer.lookupByCredentialId(credentialIdHex);
// Lookup by address
const { contracts } = await indexer.lookupByAddress('GABC...');
// Get full contract details
const details = await indexer.getContractDetails('CABC...');import type { AssembledTransaction } from 'smart-account-kit';- Node.js >= 20
- pnpm (
npm install -g pnpm) - Stellar CLI (installation guide)
# Clone the repository
git clone https://github.com/kalepail/smart-account-kit
cd smart-account-kit
# Configure demo environment (has testnet defaults)
cp demo/.env.example demo/.env
# Edit demo/.env if needed
# Install dependencies
pnpm install
# Build everything (generates bindings from network, builds packages)
pnpm run build:allThe build script reads configuration from demo/.env to generate TypeScript bindings by fetching contract metadata from the Stellar network. The demo comes pre-configured with testnet contract addresses.
Key variables in demo/.env:
VITE_RPC_URL- Stellar RPC endpointVITE_NETWORK_PASSPHRASE- Network passphraseVITE_ACCOUNT_WASM_HASH- Smart account contract WASM hashVITE_ACCOUNT_CONTRACT_ID- Optional contract ID for regenerating bindings from a deployed instance instead of a WASM hashVITE_WEBAUTHN_VERIFIER_ADDRESS- Deployed WebAuthn verifier contractVITE_ED25519_VERIFIER_ADDRESS- Deployed Ed25519 verifier contractVITE_NATIVE_TOKEN_CONTRACT- Native XLM SAC contract used by the demoVITE_THRESHOLD_POLICY_ADDRESS- Deployed threshold policy contractVITE_SPENDING_LIMIT_POLICY_ADDRESS- Deployed spending-limit policy contractVITE_WEIGHTED_THRESHOLD_POLICY_ADDRESS- Optional deployed weighted-threshold policy contractVITE_RELAYER_URL- Optional relayer proxy URL for fee-sponsored transactions
The Smart Account Kit uses contracts from OpenZeppelin's stellar-contracts. You can:
-
Use pre-deployed testnet contracts (recommended for development):
- The
demo/.env.exampleincludes the current uploaded smart-account WASM hash plus the current deployed verifier and policy addresses - The default setup intentionally uses the smart-account WASM hash instead of a fixed smart-account contract ID because smart-account deployment requires constructor args (
signersandpolicies)
- The
-
Deploy your own contracts:
- Clone stellar-contracts
- Build and deploy the contracts
- Use the resulting WASM hashes or contract IDs
Current checked-in testnet defaults live in demo/.env.example.
| Command | Description |
|---|---|
pnpm run build |
Build SDK only (requires bindings already generated) |
pnpm run build:all |
Full build: generate bindings from network + build SDK |
pnpm run build:demo |
Build SDK and demo application |
pnpm run build:watch |
Watch mode for SDK development |
pnpm run test |
Run tests |
pnpm run clean |
Remove build artifacts |
# Ensure you're logged in to npm
npm login
# Bump version (updates package.json)
pnpm version patch # or minor, major
# Publish
pnpm publish
# Or publish with specific tag
pnpm publish --tag beta- OpenZeppelin stellar-contracts - Smart account contracts this SDK interacts with
- Demo Application - Interactive demo for testing the SDK
- Indexer - Backend service for contract discovery
MIT License - see LICENSE file for details.