Automated Cross-Contract State Restoration SDK & Wallet Middleware
Soroban-Resurrect solves the "archived ledger entry" problem for Soroban dApps. When a user's persistent data (token balance, loan position, etc.) expires due to TTL rent, their transaction fails with a cryptic error. This SDK automatically detects archived entries via CAP-0066 and seamlessly restores them before submitting the user's intended transaction.
[User Action: Withdraw]
│
▼
[dApp Frontend] ──► Soroban-Resurrect
│
┌─────┴──────┐
▼ ▼
Simulate Detect
Transaction Archived
(RPC) Keys
│ │
└──────┬─────┘
▼
RestoreFootprintOp
│
┌──────┴──────┐
▼ ▼
Restore Tx Original Tx
(sign+send) (sign+send)
| Package | Description |
|---|---|
@soroban-resurrect/sdk |
Core TypeScript SDK — wraps Soroban RPC, detects archived keys, builds & executes restore transactions |
@soroban-resurrect/react-hook |
React context provider + hook for easy dApp integration (also React Native compatible) |
@soroban-resurrect/vue-hook |
Vue 3 composable for easy dApp integration |
@soroban-resurrect/svelte-hook |
Svelte store for easy dApp integration |
npm install @soroban-resurrect/sdk @stellar/stellar-sdkimport { SorobanResurrect } from '@soroban-resurrect/sdk'
import { TransactionBuilder, Account, Operation, Networks } from '@stellar/stellar-sdk'
const sr = new SorobanResurrect({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: Networks.TESTNET,
})
// Check if a transaction needs restoration
const tx = new TransactionBuilder(account, { fee: '100', networkPassphrase: Networks.TESTNET })
.addOperation(
Operation.invokeContractFunction({
contract: 'CCJZ5...K2Q',
function: 'withdraw',
args: [nativeToScVal(1000, { type: 'i128' })],
}),
)
.setTimeout(30)
.build()
const needsRestore = await sr.needsRestore(tx)
// → true if archived ledger entries are detected
// Submit with automatic restore
const wallet = {
isConnected: async () => true,
getPublicKey: async () => freighter.publicKey,
signTransaction: async (txXdr, opts) => freighter.signTransaction(txXdr, opts),
}
const result = await sr.submitWithRestore({ transaction: tx, wallet })
if (result.success) {
console.log('Original tx hash:', result.originalTxHash)
if (result.restoreTxHash) {
console.log('Restore tx hash:', result.restoreTxHash)
console.log('Archived keys restored:', result.archivedKeysDetected)
}
} else {
console.error('Failed:', result.error)
}import { SorobanResurrectProvider, useSorobanResurrectContext } from '@soroban-resurrect/react-hook'
function App() {
return (
<SorobanResurrectProvider config={{ rpcUrl: 'https://soroban-testnet.stellar.org' }}>
<WithdrawButton />
</SorobanResurrectProvider>
)
}
function WithdrawButton() {
const { submitWithRestore, state, isProcessing } = useSorobanResurrectContext()
const handleWithdraw = async () => {
const result = await submitWithRestore(tx, wallet)
// result.success, result.originalTxHash, etc.
}
return (
<button onClick={handleWithdraw} disabled={isProcessing}>
{isProcessing ? state.message : 'Withdraw'}
</button>
)
}import { useSorobanResurrect } from '@soroban-resurrect/react-hook'
function WithdrawButton() {
const { submitWithRestore, state, isProcessing } = useSorobanResurrect({
config: { rpcUrl: 'https://soroban-testnet.stellar.org' },
})
// same API as context version
}<script setup lang="ts">
import { ref } from 'vue'
import { useSorobanResurrect } from '@soroban-resurrect/vue-hook'
const config = ref({ rpcUrl: 'https://soroban-testnet.stellar.org' })
const { state, isProcessing, submitWithRestore, reset } = useSorobanResurrect(config)
</script>
<template>
<button @click="reset()" :disabled="isProcessing">
{{ isProcessing ? state.message : 'Withdraw' }}
</button>
</template><script>
import { onDestroy } from 'svelte'
import { writable } from 'svelte/store'
import { createSorobanResurrect } from '@soroban-resurrect/svelte-hook'
const config = writable({ rpcUrl: 'https://soroban-testnet.stellar.org' })
const { state, isProcessing, submitWithRestore, destroy } = createSorobanResurrect(config)
onDestroy(destroy)
</script>
<button on:click={() => submitWithRestore(tx, wallet)} disabled={$isProcessing}>
{$state.message || 'Withdraw'}
</button>All hooks in @soroban-resurrect/react-hook use only standard React APIs (useState, useCallback, useRef, useEffect) and contain no browser-specific code, making them fully compatible with React Native out of the box. See examples/react-native/ for a complete example.
For the full picture — system diagram, data flow, state machine, and component interaction, all with Mermaid diagrams — see
ARCHITECTURE.md.
The SDK implements the complete CAP-0066 restoration flow:
- Simulate — Calls
simulateTransactionon the Soroban RPC endpoint - Detect — Checks if the response is a
SimulateTransactionRestoreResponse(viaApi.isSimulationRestore()); extracts the archived ledger keys from theSorobanDataBuilderfootprint - Build Restore Tx — Constructs a transaction with
Operation.restoreFootprint({})using the restore preamble data (footprint + resource fee from simulation) - Wallet Sign #1 — Prompts the user's wallet to sign the restore transaction
- Submit Restore — Sends the restore transaction and polls
getTransactionuntil confirmed - Rebuild Original — After restore confirms, re-simulates the original transaction, rebuilds it with a fresh account sequence number and the new simulation data via
assembleTransaction - Wallet Sign #2 — Prompts the wallet to sign the prepared original transaction
- Submit Original — Sends the user's intended transaction
- Simulation errors are returned immediately with the error message
- Restore transaction failure returns the restore tx hash and error details
- Re-simulation failure after successful restore returns an error (unusual — indicates the restore wasn't sufficient)
- All exceptions are caught and returned as structured
ResurrectResultobjects
A more detailed reference — including
@throwsdocumentation and cross-links between related methods — lives indocs/API.md. Every public export also carries full JSDoc in source.
constructor(config: SorobanResurrectConfig)
// Properties
server: rpc.Server // The underlying Soroban RPC server
config: Required<SorobanResurrectConfig>
state: RestoreState // Current state machine state
stateInfo: RestoreStateInfo // State + message + archived keys + error
// Methods
simulate(transaction: Transaction): Promise<SimulateResponse>
detectArchivedKeys(transaction: Transaction): Promise<ArchivedLedgerEntry[]>
needsRestore(transaction: Transaction): Promise<boolean>
buildRestoreTx(sourcePublicKey: string, transaction: Transaction): Promise<Transaction>
estimateRestoreCost(transaction: Transaction): Promise<RestoreCostEstimate>
submitWithRestore(options: SubmitWithRestoreOptions): Promise<ResurrectResult>
reset(fromState?: RestoreState): void
onStateChange(listener: (info: RestoreStateInfo) => void): () => void // unsubscribe// Context Provider
<SorobanResurrectProvider config={config}>
{children}
</SorobanResurrectProvider>
// Context Hook
useSorobanResurrectContext(): {
resurrect: SorobanResurrect | null
config: SorobanResurrectConfig
state: RestoreStateInfo
isProcessing: boolean
submitWithRestore(tx, wallet): Promise<ResurrectResult>
detectArchivedKeys(tx): Promise<ArchivedLedgerEntry[]>
reset(fromState?: RestoreState): void
}
// Standalone Hook
useSorobanResurrect({ config }): UseSorobanResurrectReturn // same shapeinterface SorobanResurrectConfig {
rpcUrl: string
networkPassphrase?: string // default: Testnet
pollIntervalMs?: number // default: 1000
pollTimeoutMs?: number // default: 60000
}
interface WalletAdapter {
isConnected(): Promise<boolean>
getPublicKey(): Promise<string>
signTransaction(tx: string, opts?: { networkPassphrase?: string }): Promise<string>
}
interface ResurrectResult {
success: boolean
originalTxHash?: string
restoreTxHash?: string
archivedKeysDetected: number
error?: string
}
type RestoreState =
| 'idle'
| 'simulating'
| 'restore_needed'
| 'signing_restore'
| 'submitting_restore'
| 'confirming_restore'
| 'signing_original'
| 'submitting_original'
| 'success'
| 'error'# Install dependencies
npm install
# Build all packages
npm run build
# Run tests
npm test
# Type-check
npm run typecheck
# Run example app
npm run dev:example
# Run the documentation site locally
npm run docs:devThe full documentation site (getting started guide, complete API reference, interactive examples, tutorial, and framework integration guides) lives in docs/ and is built with VitePress. Run it locally with npm run docs:dev, or build the static site with npm run docs:build.
├── packages/
│ ├── sdk/ # @soroban-resurrect/sdk
│ │ ├── src/
│ │ │ ├── SorobanResurrect.ts # Main class
│ │ │ ├── Archiver.ts # Archived key detection
│ │ │ ├── Restorer.ts # Restore tx builder
│ │ │ ├── Executor.ts # Full execution flow
│ │ │ ├── types.ts # Type definitions
│ │ │ ├── constants.ts # Defaults
│ │ │ ├── index.ts # Exports
│ │ │ └── __tests__/ # Unit tests (vitest)
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── react-hook/ # @soroban-resurrect/react-hook
│ ├── src/
│ │ ├── SorobanResurrectContext.tsx
│ │ ├── useSorobanResurrect.ts
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
├── examples/basic/ # Vite + React demo app
├── .github/workflows/ci.yml
├── package.json # Root workspace config
└── tsconfig.json
See MIGRATION.md for breaking changes and upgrade steps between versions.
MIT