Skip to content

DevelopersHelpDesk/soroban-test-utils

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

soroban-test-utils

A comprehensive testing and developer utilities library for Soroban smart contracts.

What this is: The community-facing equivalent of what Hardhat/Foundry did for Ethereum — a well-maintained, open-source collection of test helpers, mock environments, and assertion utilities purpose-built for Soroban.

Why it exists: Soroban introduced genuinely novel concepts — especially state expiration (ledger entries that don't persist indefinitely) — with almost no tooling or guidance. This library fixes that gap.


Install

npm install soroban-test-utils
# or
yarn add soroban-test-utils

At a glance

Module What it does
SorobanTestClient Deploy, invoke, and read state on testnet/mainnet/futurenet
MockLedger In-memory ledger for fast, offline unit tests — no network
assertResult() Fluent assertions on invoke/simulation results
assertContractData() Assert on contract state and expiration
ExpirationSimulator Test expiration edge cases deterministically, offline
ExpirationAnalyzer Analyze live TTL state via RPC
ResourceProfiler Track and compare resource usage across invocations
ledgersToTime() Convert ledger counts ↔ human time
NETWORK, LEDGERS_PER_DAY, ... All Soroban protocol constants in one place

Quick start

1. Testnet integration test

import { SorobanTestClient, assertResult, assertContractData } from 'soroban-test-utils';

const client = await SorobanTestClient.create('testnet');

// Fund a fresh keypair via Friendbot
const deployer = await client.generateFundedKeypair();

// Deploy your contract
const deployed = await client.deploy({
  wasm: './target/wasm32-unknown-unknown/release/my_token.wasm',
  deployer,
});

// Invoke a method
const result = await client.invoke<bigint>({
  contractId: deployed.contractId,
  method: 'balance',
  args: [deployer.publicKey()],
  caller: deployer,
});

// Fluent assertions
assertResult(result)
  .toReturnBigInt(0)
  .toHaveEmittedNoEvents()
  .toUseFewerThanCpuInstructions(5_000_000)
  .toBeWithinResourceLimits(80);

// Assert on contract state
const entry = await client.getContractData(
  deployed.contractId,
  { tag: 'Balance', address: deployer.publicKey() },
  'persistent'
);

assertContractData(entry)
  .toExist()
  .toHaveValue(0n)
  .toHaveLedgersRemaining(1_000, await client.getCurrentLedger());

2. Offline unit tests with MockLedger

No network. No RPC. No waiting. Just a predictable in-memory state machine.

import { MockLedger, assertContractData, LEDGERS_PER_DAY } from 'soroban-test-utils';

const CONTRACT = 'CTEST...';

it('balance persists across 30 days', () => {
  const ledger = new MockLedger({ startingSequence: 1_000 });

  ledger.setContractData(CONTRACT, 'BALANCE', 5_000n, 'persistent');
  ledger.advanceLedgers(LEDGERS_PER_DAY * 30);

  assertContractData(ledger.getContractData(CONTRACT, 'BALANCE'))
    .toExist()
    .toHaveValue(5_000n);
});

it('temporary session token expires', () => {
  const ledger = new MockLedger({ startingSequence: 1_000 });

  ledger.setContractData(CONTRACT, 'SESSION', 'token_abc', 'temporary', {
    expiresAt: 1_050,
  });

  ledger.advanceLedgers(60); // now at 1060, past expiry

  assertContractData(ledger.getContractData(CONTRACT, 'SESSION'))
    .toBeExpiredBy(ledger.sequence);
});

State expiration — the painful part

Soroban is the only smart contract platform where ledger entries expire. Every piece of contract storage has a TTL. This trips up developers coming from Ethereum.

There are three key concepts:

Concept Temporary Persistent
Can expire? ✅ Yes ✅ Yes
Can be restored after expiry? Never ✅ Within restore window
Default min TTL ~85 seconds ~4 months

Test expiration scenarios offline

import { ExpirationSimulator, ledgersToTime, MIN_TEMPORARY_TTL } from 'soroban-test-utils';

const sim = new ExpirationSimulator(100_000);

// Scenario: temporary entry cliff — gone forever after expiry
const cliff = sim.temporaryExpiryCliff('CTEST', 'SESSION_TOKEN', MIN_TEMPORARY_TTL);
console.log(cliff.wasAlive);       // true
console.log(cliff.isGoneForever);  // true
console.log(cliff.canRestore);     // false — NEVER for temporary entries

// Scenario: persistent entry within restore window
sim.reset(100_000);
const restore = sim.persistentRestoreWindow('CTEST', 'USER_PROFILE', 200);
console.log(restore.wasRestoredSuccessfully); // true
console.log(restore.newExpiresAt);            // extended by MIN_PERSISTENT_TTL

// Scenario: TTL bump keeps an entry alive
sim.reset(100_000);
const bump = sim.ttlBumpKeepsAlive('CTEST', 'CRITICAL_STATE', {
  ttl: 500,
  bumpAt: 300,
  bumpBy: 5_000,
  checkAfter: 2_000,
});
console.log(bump.isAliveAtEnd); // true

Time conversion utilities

import { ledgersToTime, timeToLedgers, LEDGERS_PER_DAY } from 'soroban-test-utils';

ledgersToTime(17);
// { seconds: 85, minutes: 1.4, hours: 0.02, days: 0.001, description: '~1.4 minute(s)' }

timeToLedgers({ days: 7 });   // → 120960 ledgers
timeToLedgers({ hours: 1 });  // → 720 ledgers

Test fixture with all expiration states

import { buildExpirationScenario } from 'soroban-test-utils';

const { ledger, contractId, entries } = buildExpirationScenario(50_000);

// entries.alive             — healthy, long-lived
// entries.expiringSoon      — expires within 1 hour
// entries.temporary         — short-lived temporary entry
// entries.expiredRestorable — expired but within restore window
// entries.expiredGone       — expired and past restore window

Analyze live entries via RPC

import { SorobanTestClient, ExpirationAnalyzer } from 'soroban-test-utils';

const client = await SorobanTestClient.create('testnet');
const analyzer = new ExpirationAnalyzer(client);

// Check if an entry will expire within 7 days
const willExpire = await analyzer.willExpireWithinTime(
  contractId, 'USER_DATA', { days: 7 }
);

// Get a human-readable description
const desc = await analyzer.describeExpiration(contractId, 'USER_DATA');
// → "⚠️  EXPIRING SOON: Expires at ledger 123456 (~2.5 hour(s) remaining, 1800 ledgers)."

Resource profiling

Track resource consumption across invocations and catch regressions before they hit production.

import { ResourceProfiler, SorobanTestClient } from 'soroban-test-utils';

const client = await SorobanTestClient.create('testnet');
const profiler = new ResourceProfiler();

const v1 = await client.simulate({ contractId: 'COLD_ADDR', method: 'transfer', args: [] });
profiler.record('transfer_v1', v1.resources);

const v2 = await client.simulate({ contractId: 'CNEW_ADDR', method: 'transfer', args: [] });
profiler.record('transfer_v2', v2.resources);

// Print a detailed report
console.log(profiler.report('transfer_v1'));
// Resource Profile: transfer_v1
// ─────────────────────────────────────────────────────
//   CPU Instructions        1,000,000  [█░░░░░░░░░░░░░░░░░░░] 1%
//   Memory Bytes              500,000  [░░░░░░░░░░░░░░░░░░░░] 1%
//   ...

// Compare v1 vs v2
const diff = profiler.compare('transfer_v1', 'transfer_v2');
console.log(diff.summary);
// Comparison: transfer_v1 → transfer_v2
// Verdict: ✅ IMPROVED
// ✅ CPU Instructions         1,000,000 →       800,000  (-20%)
// ➖ Memory Bytes               500,000 →       500,000  (0%)
// ...

// Assert v2 stays under 80% of protocol limits (safe for CI)
profiler.assertWithinLimits('transfer_v2', 80);

Assertion reference

assertResult(result)

Chainable fluent assertions on InvokeResult or SimulationResult.

assertResult(result)
  // Return value
  .toReturnValue(42n)
  .toReturnBigInt(42)          // accepts number, converts to bigint
  .toBeTruthy()
  .toBeEmpty()
  .toSatisfy(v => v > 0n)
  .not.toReturnValue(0n)       // negation

  // Simulation-specific
  .toSucceed()
  .toFail('insufficient balance')

  // Events
  .toHaveEmittedEvent({ topics: ['transfer', 'alice'] })
  .toHaveEmittedEventCount(2)
  .toHaveEmittedNoEvents()

  // Resources
  .toUseFewerThanCpuInstructions(5_000_000)
  .toUseFewerThanMemoryBytes(1_000_000)
  .toWriteFewerThanBytes(10_000)
  .toPayFewerThanStroops(5_000)
  .toBeWithinResourceLimits(80); // all resources under 80% of protocol limits

assertContractData(entry)

assertContractData(entry)
  .toExist()
  .toNotExist()
  .toHaveValue(1_000n)
  .toExpireAfterLedger(currentLedger)
  .toBeExpiredBy(someKnownLedger)
  .toHaveLedgersRemaining(1_000, currentLedger);

Jest custom matchers

// In jest.setup.ts
import { registerJestMatchers } from 'soroban-test-utils';
registerJestMatchers();

// In tests
expect(result).toReturnSorobanValue(42n);
expect(result).toHaveEmittedSorobanEvent({ topics: ['transfer'] });
expect(result).toHaveCpuInstructionsBelow(5_000_000);

Protocol constants reference

import {
  NETWORK,                          // { TESTNET, MAINNET, FUTURENET, STANDALONE }
  RPC_URLS,                         // { TESTNET, MAINNET, FUTURENET }
  LEDGER_CLOSE_TIME_SECONDS,        // 5
  LEDGERS_PER_MINUTE,               // 12
  LEDGERS_PER_HOUR,                 // 720
  LEDGERS_PER_DAY,                  // 17_280
  LEDGERS_PER_WEEK,                 // 120_960
  MIN_TEMPORARY_TTL,                // 17 ledgers (~85 seconds)
  MAX_TEMPORARY_TTL,                // 17_280 ledgers (~1 day)
  MIN_PERSISTENT_TTL,               // ~4 months in ledgers
  MAX_PERSISTENT_TTL,               // ~1 year in ledgers
  PERSISTENT_RESTORE_WINDOW_LEDGERS,// ~7 days
  STROOPS_PER_XLM,                  // 10_000_000
  MAX_CPU_INSTRUCTIONS,             // 100_000_000
  MAX_MEMORY_BYTES,                 // 41_943_040 (40 MB)
  MAX_LEDGER_READ_BYTES,            // 200_000
  MAX_LEDGER_WRITE_BYTES,           // 66_560
} from 'soroban-test-utils';

MockLedger API

const ledger = new MockLedger({ startingSequence: 1_000 });

// Ledger control
ledger.advanceLedgers(500);
ledger.advanceTo(2_000);
ledger.sequence; // → current ledger
ledger.timestamp; // → current Unix timestamp

// Contract data
ledger.setContractData(contractId, key, value, 'persistent', { expiresAt: 5_000 });
ledger.getContractData(contractId, key);
ledger.deleteContractData(contractId, key);
ledger.bumpContractDataTTL(contractId, key, 1_000); // extend by 1000 ledgers
ledger.restoreContractData(contractId, key);        // persistent only
ledger.getContractEntries(contractId);              // all entries for a contract

// Expiration analysis
ledger.getExpiredEntries();
ledger.getEntriesExpiringWithin(1_000);
ledger.expirationReport(); // human-readable text report

// Account mocking
ledger.setAccount(publicKey, { balance: 10_000_000_000n, sequence: 1n });
ledger.getAccount(publicKey);

// Event log
ledger.emitEvent(contractId, topics, data);
ledger.getEvents(contractId);
ledger.clearEvents();

// Reset
ledger.reset({ startingSequence: 1_000 });

SorobanTestClient API

const client = await SorobanTestClient.create('testnet');
// or: SorobanTestClient.create({ rpcUrl: '...', network: NETWORK.STANDALONE });

// Account management
await client.fundAccount(publicKey);
const keypair = await client.generateFundedKeypair();

// Contract lifecycle
const deployed = await client.deploy({ wasm, deployer, salt?, initArgs? });
const result   = await client.invoke({ contractId, method, args?, caller, fee? });
const simResult = await client.simulate({ contractId, method, args? });

// State inspection
const ledger = await client.getCurrentLedger();
const info   = await client.getLedgerInfo();
const entry  = await client.getContractData(contractId, key, 'persistent');
const entries = await client.getLedgerEntries([xdrKeyBase64]);

Comparison to existing tools

Feature soroban-test-utils Stellar CLI Lab 4.0 (SDF internal)
Offline unit tests ✅ MockLedger
State expiration testing ✅ ExpirationSimulator planned
Fluent assertions ✅ assertResult
Resource profiling ✅ ResourceProfiler limited planned
Jest integration
TypeScript-first partial unknown
Open source, community

Contributing

PRs welcome. Key areas for contribution:

  • More mock entry types — trustlines, claimable balances
  • Snapshot testing — assert that state hasn't changed between runs
  • Gas estimation utilities — predictive fee budgeting
  • Multi-contract interaction mocks — cross-contract call simulation
  • CLI toolsoroban-test profile <wasm> for quick resource profiling

License

MIT

About

A JS/TS testing library for Soroban smart contracts — mock ledger, assertion helpers, state expiration simulation, and resource profiling. The Hardhat/Foundry equivalent for Stellar.

Topics

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors