{"id":50765127,"url":"https://github.com/lobotomoe/soroban-fork","last_synced_at":"2026-06-11T13:01:40.885Z","repository":{"id":354110893,"uuid":"1218940277","full_name":"lobotomoe/soroban-fork","owner":"lobotomoe","description":"Lazy-loading mainnet/testnet fork for Soroban tests. Anvil-equivalent for Stellar Soroban.","archived":false,"fork":false,"pushed_at":"2026-04-27T07:35:05.000Z","size":80,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-27T08:18:37.073Z","etag":null,"topics":["anvil","blockchain","mainnet-fork","rust","soroban","stellar","testing"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lobotomoe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-23T11:18:08.000Z","updated_at":"2026-04-27T07:35:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lobotomoe/soroban-fork","commit_stats":null,"previous_names":["lobotomoe/soroban-fork"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/lobotomoe/soroban-fork","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lobotomoe%2Fsoroban-fork","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lobotomoe%2Fsoroban-fork/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lobotomoe%2Fsoroban-fork/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lobotomoe%2Fsoroban-fork/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lobotomoe","download_url":"https://codeload.github.com/lobotomoe/soroban-fork/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lobotomoe%2Fsoroban-fork/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34199516,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anvil","blockchain","mainnet-fork","rust","soroban","stellar","testing"],"created_at":"2026-06-11T13:01:39.799Z","updated_at":"2026-06-11T13:01:40.860Z","avatar_url":"https://github.com/lobotomoe.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# soroban-fork\n\n[![crates.io](https://img.shields.io/crates/v/soroban-fork.svg)](https://crates.io/crates/soroban-fork)\n[![docs.rs](https://docs.rs/soroban-fork/badge.svg)](https://docs.rs/soroban-fork)\n[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)\n\nLazy-loading mainnet/testnet fork for Soroban tests. Think [Foundry's Anvil](https://book.getfoundry.sh/anvil/), but for Stellar Soroban.\n\nWhen a test reads a ledger entry that isn't in the local cache, `soroban-fork` fetches it from the Soroban RPC on the fly. No need to pre-snapshot every contract your test might touch.\n\n## Install\n\n```toml\n[dev-dependencies]\nsoroban-fork = \"0.8\"\n```\n\n## Usage\n\n```rust,no_run\nuse soroban_fork::ForkConfig;\nuse soroban_sdk::{Address, String, Symbol, vec};\n\n#[test]\nfn test_against_real_state() {\n    let env = ForkConfig::new(\"https://soroban-testnet.stellar.org:443\")\n        .cache_file(\"test_cache.json\")   // optional: persist for faster reruns\n        .build()\n        .expect(\"fork setup\");\n\n    env.mock_all_auths();\n\n    let contract = Address::from_string(\u0026String::from_str(\n        \u0026env,\n        \"CABC...YOUR_CONTRACT_ID\",\n    ));\n\n    // This lazily fetches the contract's instance, WASM code,\n    // and any storage entries from the real network.\n    let result: i128 = env.invoke_contract(\n        \u0026contract,\n        \u0026Symbol::new(\u0026env, \"total_assets\"),\n        vec![\u0026env],\n    );\n\n    assert!(result \u003e= 0);\n\n    // env.fetch_count() tells you how many RPC calls were made.\n    // Cache is auto-saved on drop (includes lazy-fetched entries).\n}\n```\n\n## How it works\n\n```\nYour test calls contract.total_assets()\n         |\n         v\n  Soroban VM needs a ledger entry\n         |\n         v\n  RpcSnapshotSource.get(key)\n         |\n    +----+----+\n    |         |\n  Cache     Cache miss\n  hit         |\n    |         v\n    |    getLedgerEntries RPC call\n    |         |\n    |         v\n    |    Cache result locally\n    |         |\n    +----+----+\n         |\n         v\n  Return entry to VM\n```\n\n- **First run**: entries are fetched from the Soroban RPC as needed. Each unique entry = one HTTP call (batched in chunks of 200 if pre-fetching).\n- **Subsequent runs**: if `cache_file` is set, entries are loaded from disk. Only new entries trigger RPC calls.\n- **State changes are local**: the real network is never modified. Deposits, transfers, and other mutations happen in memory only.\n\n## API\n\n### `ForkConfig`\n\n```rust\nForkConfig::new(rpc_url)           // Soroban RPC endpoint\n    .cache_file(\"cache.json\")             // optional: disk persistence + auto-save on drop\n    .network_id(bytes)                    // optional: override the SHA-256 network id\n    .fetch_mode(FetchMode::Strict)        // optional: Strict (default) or Lenient\n    .at_ledger(1_234_567)                 // optional: pin the Env's reported sequence\n    .pinned_timestamp(1_700_000_000)      // optional: pin the Env's close time\n    .max_protocol_version(25)             // optional: cap the protocol the VM reports\n    .tracing(true)                        // optional: capture cross-contract call tree\n    .rpc_config(RpcConfig { retries: 5, ..RpcConfig::default() })\n    .build()?                             // returns Result\u003cForkedEnv, ForkError\u003e\n```\n\nNetwork metadata (passphrase + SHA-256 id) is fetched from the RPC's\n`getNetwork` method at build time — no URL heuristics, no silent defaults.\nOverride with `.network_id(bytes)` only if you actually need to.\n\nThe `Env`'s reported timestamp defaults to the close time of the latest\nledger, fetched via `getLedgers` at build time. Tests are reproducible\nacross runs out of the box — pin an explicit value via\n`.pinned_timestamp(...)` only when you need to anchor to a specific\nmoment (e.g. reproducing a known historical scenario).\n\n### `ForkedEnv`\n\nReturned by `ForkConfig::build()`. Implements `Deref\u003cTarget = Env\u003e` so all SDK\nmethods work transparently. Adds fork-specific capabilities:\n\n```rust,ignore\nlet env = ForkConfig::new(rpc_url).cache_file(\"cache.json\").build()?;\n\n// Use like a regular Env (via Deref)\nenv.mock_all_auths();\nlet result: i128 = env.invoke_contract(\u0026addr, \u0026symbol, vec![\u0026env]);\n\n// Fork-specific methods\nenv.fetch_count();                 // number of RPC calls made\nenv.save_cache()?;                 // explicit save (also called automatically on drop)\nenv.warp_time(86_400);             // advance ledger timestamp + sequence\nenv.deal_token(\u0026usdc, \u0026who, amt);  // Foundry-style balance deal\nenv.env();                         // \u0026Env (for edge cases where Deref doesn't suffice)\n```\n\n### `FetchMode`\n\nControls behavior when the RPC fails from inside the VM loop (where the\n`SnapshotSource` trait can't return a typed error):\n\n- **`Strict`** (default): panic. Best for tests — a fetch failure means the test setup is wrong, and you want the stack trace.\n- **`Lenient`**: log at `warn!` level and return `None`. Useful when partial state is acceptable.\n\n### `RpcConfig`\n\nTransport tunables. Defaults: 3 retries with 300 ms exponential backoff\nplus full jitter (so concurrent test runners don't synchronise their\nretries into a thundering herd), 30 s per-request timeout, 200-key batch\nsize (Soroban RPC cap). Customize via `.rpc_config(RpcConfig { .. })` on\nthe builder. HTTP 408, 425, 429, and 5xx responses are retried; other\n4xx codes fail fast and include the response body for diagnostics.\n\n### Tracing — Foundry-style call trees\n\nSet `.tracing(true)` on the builder to capture cross-contract call trees.\nThe host runs in `DiagnosticLevel::Debug`, every `fn_call`/`fn_return`\nemits a diagnostic event, and `env.trace()` reconstructs the tree:\n\n```rust\nlet env = ForkConfig::new(rpc_url)\n    .tracing(true)\n    .build()?;\n\nenv.invoke_contract::\u003ci128\u003e(\u0026vault, \u0026Symbol::new(\u0026env, \"deposit\"), args);\nenv.print_trace();\n```\n\n```text\n[TRACE]\n  [CABC…XYZ1] deposit(GACC…QRST, 1000000)\n    [CCDE…UVW2] transfer_from(GACC…QRST, CABC…XYZ1, 1000000)\n      ← ()\n    [CFGH…IJK3] invest(1000000)\n      ← 1010000\n    ← 1010000\n```\n\nProgrammatic access via `env.trace()` returns a `Trace` with structured\n`TraceFrame`s — useful for asserting call structure or balances inside\na test. Failed calls render as `[rolled back]`; WASM traps show as\n`TRAPPED (no fn_return)`.\n\n**Per-invocation scoping.** The host's `InvocationMeter` clears the\nevents buffer at the start of every top-level `invoke_contract`, so each\n`trace()` reflects only the most recent top-level call. Capture before\nthe next call if you need history. See the\n[`trace` module docs](https://docs.rs/soroban-fork/latest/soroban_fork/trace/index.html)\nfor wire-format details and caveats (single-`Vec`-arg ambiguity).\n\n### Auth introspection — `print_auth_tree`\n\nWhen debugging cross-contract authorization (`Error(Auth, InvalidAction)`,\nunexpected `require_auth` panics, \"did Alice actually have to sign this?\"),\nread out the recording auth manager's payload set:\n\n```rust\nlet env = ForkConfig::new(rpc_url).build()?;\nenv.mock_all_auths();\n\nenv.invoke_contract::\u003c()\u003e(\u0026usdc, \u0026Symbol::new(\u0026env, \"transfer\"), args);\n\nenv.print_auth_tree();\n```\n\n```text\n[AUTH]\n  payload #0  signer=GA62…J3CT  nonce=5541220902715666415\n    [CCW6…MI75] transfer(GA62…J3CT, GB7O…7AMM, 250000000)\n```\n\nProgrammatic access via `env.auth_tree()` returns an\n[`AuthTree`](https://docs.rs/soroban-fork/latest/soroban_fork/auth_tree/struct.AuthTree.html)\nwith `payload_count()` / `invocation_count()` / `is_empty()` accessors —\nuseful for asserting that a multi-hop call demanded exactly the expected\nnumber of `require_auth`s. Raw `Vec\u003cRecordedAuthPayload\u003e` is available\nvia `env.auth_payloads()`.\n\n**Per-invocation scoping**, like `trace()`: only payloads from the most\nrecent top-level `invoke_contract` are visible.\n\n**Limits inherited from the upstream host crate**, documented honestly:\n\n- `Error(Auth, InvalidAction)` carries only the address — the failed\n  contract / function / expected authorizer are constructed locally\n  inside the host and not persisted to any accessor we can read out.\n  After a failed call, `auth_tree()` reflects whatever payload set the\n  host left in its `previous_authorization_manager`; the exact contents\n  after a panic mid-invocation are an implementation detail of\n  `soroban-env-host`. A structured `last_auth_failure()` awaits an\n  upstream change.\n- Whether `mock_all_auths_allowing_non_root_auth` was used is not\n  exposed by the host. The README's \"Common pitfalls\" section documents\n  the trap; an enforceable `strict_auth` mode awaits an upstream change.\n\n## JSON-RPC server mode\n\nLibrary mode (everything above) is for Rust tests. **Server mode** turns\nsoroban-fork into a Stellar Soroban RPC drop-in that any tooling — JS,\nPython, Go SDKs, Stellar Lab, Freighter, custom clients — can point at:\n\n```sh\ncargo install soroban-fork --features server\nsoroban-fork serve --rpc https://soroban-rpc.mainnet.stellar.gateway.fm\n# → serving JSON-RPC on http://127.0.0.1:8000\n```\n\nThen any client speaking the Stellar RPC dialect:\n\n```js\nimport { SorobanRpc } from \"@stellar/stellar-sdk\";\nconst server = new SorobanRpc.Server(\"http://localhost:8000\");\nconst account = await server.getAccount(\"GA5...\");\nconst result = await server.simulateTransaction(tx);  // hits the fork\n```\n\nOr via the Rust `stellar-rpc-client`, raw curl, or anything that\nunderstands the spec.\n\n### Pre-funded test accounts *(new in v0.7)*\n\nThe fork mints **10 deterministic test accounts** at build time, each\nwith 100K XLM and a USDC trustline ready to receive. Same seed\nproduces the same accounts every run, so test code can hard-code\naddresses by index. The CLI prints them on startup:\n\n```\nsoroban-fork v0.7\nListening on http://127.0.0.1:8000\n\nAvailable test accounts:\n(0) GBXXX...AB12  (100000.0000000 XLM)  -\u003e  SAXXX...CD34\n(1) GCYYY...EF56  (100000.0000000 XLM)  -\u003e  SAXXX...GH78\n...\n```\n\nPass them to JS-SDK's `Keypair.fromSecret(...)` to sign envelopes.\nAfter every successful `sendTransaction`, the source account's\nsequence number auto-increments — so chained `getAccount` →\n`TransactionBuilder` → `sendTransaction` loops just work.\n\n**Real DEX flow works end-to-end.** A test account can swap XLM →\nUSDC against the live Phoenix DEX (or Soroswap, Aquarius, …) and\nthe USDC actually lands in its trustline. Smoke-tested:\n1000 XLM → 167.4020548 USDC at the live mainnet pool reserves.\n\n**No hidden hardcode.** The trustline default targets the mainnet\nUSDC issuer (Circle); for testnet, futurenet, or a custom fork,\noverride via `ForkConfig::test_account_trustlines(vec![...])`. The\ntrustlines are written with `flags = AUTHORIZED_FLAG`, `limit = i64::MAX` —\nshape-equivalent to running `ChangeTrust` then having the issuer\nauthorize, just bootstrapped at build time. Auth runs in trust mode\n(`Recording(false)`) so unsigned envelopes from test code apply\nwithout ceremony.\n\nOverride count via `--accounts N` (set to `0` to disable). For\nlibrary users, `ForkConfig::test_account_count(n).build()` exposes\nthe same machinery; read accounts back with `env.test_accounts()`.\n\n### Deploy your own contracts onto the fork *(new in v0.7)*\n\nThe same `sendTransaction` accepts `HostFunction::UploadContractWasm`\nand `HostFunction::CreateContract`, so you can deploy custom contracts\nstraight onto the forked mainnet state and have them call live\nproduction contracts. The test suite's\n`server_deploy_and_invoke_custom_contract` covers the full loop:\n\n1. Upload a tiny `add(i32, i32) -\u003e i32` WASM\n2. Create the contract instance from the uploaded hash\n3. Invoke `add(2, 3)` on the deployed contract — returns 5\n\nCross-protocol scenarios (your contract calls Blend, Phoenix,\nSoroswap, etc.) follow the same pattern: dependencies the deployed\ncontract reaches into get lazy-fetched from mainnet and cached\nlocally.\n\n### The headline showcase: cheatcode-only deploy\n\nWhat makes the toolset matter, in one test:\n\n1. **`fork_setCode`** installs your WASM bytes (no `UploadContractWasm` envelope)\n2. **`fork_setStorage`** installs the contract instance entry pointing at that WASM, at a synthetic contract address (no `CreateContract` envelope, no source-account juggling, no salt)\n3. **`simulateTransaction`** invokes your cheatcode-deployed contract, returns the result\n4. The same fork still serves live mainnet contracts — `XLM SAC.decimals()` returns `7`, your `synth_contract.add(2,3)` returns `5`, both in the same simulation context\n\nTwo cheatcode calls and a contract is callable. That's the Foundry-`vm.etch`-equivalent — the headline reason this toolset exists. Live in [`server_cheatcode_only_deploy_coexists_with_mainnet`](tests/server_smoke.rs); end-to-end against mainnet, ~70 LoC.\n\n### Methods supported in v0.9.2\n\n- **`getHealth`** — fork status + latest ledger\n- **`getVersionInfo`** — server version + protocol version\n- **`getNetwork`** — passphrase + protocol version + network ID (proxied\n  from the upstream RPC at fork-build time, then served locally)\n- **`getLatestLedger`** — fork's reported ledger sequence + protocol\n- **`getLedgers`** — single-element page describing the fork point with\n  real `ledgerCloseTime` (Unix-seconds string, per Stellar convention)\n- **`getLedgerEntries`** — base64-XDR `LedgerKey` array → array of\n  entries; routed through the fork's lazy-fetch cache, so first hit\n  proxies upstream and subsequent hits are local\n- **`simulateTransaction`** — accepts a base64-XDR `TransactionEnvelope`\n  with one `InvokeHostFunctionOp`, runs it via the host's recording-mode\n  primitive, returns:\n  - `results[0].xdr` — the function's return value (`ScVal`)\n  - `results[0].auth` — auth entries `sendTransaction` would need\n  - `transactionData` — `SorobanTransactionData` with recorded footprint\n    and `resourceFee` matching `minResourceFee`\n  - `events` — diagnostic events emitted during simulation\n  - `cost.cpuInsns` / `cost.memBytes` — real numbers from the host's\n    `Budget`, *not* a `write_bytes` proxy\n  - `minResourceFee` — derived from the live on-chain Soroban fee\n    schedule via `compute_transaction_resource_fee` (since v0.5.2)\n  - `latestLedger` — fork's reported ledger\n- **`sendTransaction`** *(new in v0.6)* — applies the host invocation's\n  writes back to the snapshot source so subsequent reads see them.\n  Auth runs in trust mode (`Recording(false)`) so unsigned envelopes\n  from test code apply without ceremony. Returns `status`\n  (`\"SUCCESS\"` / `\"ERROR\"`), `hash` (sha256 of the envelope),\n  `appliedChanges` (number of `LedgerEntryChange`s written), and\n  the original envelope echo.\n- **`getTransaction`** *(new in v0.6)* — receipt lookup by hash.\n  Returns `\"SUCCESS\"` / `\"FAILED\"` / `\"NOT_FOUND\"`, plus the original\n  envelope, the host function's `ScVal` return value, and the\n  applied-changes count when found.\n\n#### Fork-mode extensions (`fork_*`)\n\nNon-standard methods, only available against soroban-fork. The\n`fork_` prefix marks the namespace boundary explicitly so a\nclient can distinguish \"this works against any Stellar RPC\" from\n\"this only works against the fork.\"\n\n- **`fork_setLedgerEntry`** *(new in v0.8, renamed from `anvil_setLedgerEntry` in v0.8.1)* —\n  force-write a base64-XDR `LedgerEntry` to any `LedgerKey`\n  directly in the snapshot source, bypassing host-level checks.\n  Load-bearing primitive for stress-test scenarios — oracle price\n  manipulation, force-set token balances, replace contract code,\n  all reduce to this one entry write.\n- **`fork_setStorage`** *(new in v0.8.2)* — sugar over\n  `fork_setLedgerEntry` for the common case of writing into a\n  contract's storage. Takes `contract` (strkey), `key` (base64\n  ScVal), `value` (base64 ScVal), optional `durability`\n  (`\"persistent\"` (default) / `\"temporary\"`), and optional\n  `liveUntilLedgerSeq`. The handler builds the `ContractData`\n  XDR server-side so clients don't have to assemble the\n  multi-level enum nesting themselves. Use this for oracle\n  price overrides and contract-storage scenarios.\n- **`fork_setCode`** *(new in v0.8.3)* — upload WASM bytes as a\n  `ContractCode` ledger entry, keyed by sha256 of those bytes.\n  Takes `wasm` (base64) and optional `liveUntilLedgerSeq`.\n  Returns `{ ok, hash, latestLedger }` — the hash is server-derived\n  (the host computes it the same way), so callers can wire a\n  follow-up `CreateContract` (or `fork_setStorage` over a\n  `ContractInstance` ScVal) to point at the uploaded code without\n  any host invocation.\n- **`fork_setBalance`** *(new in v0.8.4, Soroban-token path added in v0.8.7)* —\n  Foundry's `deal()`-equivalent for Stellar. Three asset shapes:\n  - `\"native\"` (default) — XLM, balance lives on `AccountEntry`.\n    Auto-creates the account with master threshold 1 if missing.\n  - `{ code, issuer }` — Classic credit asset (USDC, EURC, …),\n    balance lives on `TrustLineEntry`. Auto-creates the trustline\n    with `flags = AUTHORIZED`, `limit = i64::MAX` — equivalent to\n    having run `ChangeTrust` and the issuer authorising.\n  - `{ contract }` *(v0.8.7)* — any SEP-41-shaped Soroban token\n    (the SAC for Classic assets, custom Soroban tokens like BLND).\n    Handler simulates `balance(to)`, computes the delta, and\n    invokes `mint(to, delta)` or `burn(to, |delta|)` with\n    trust-mode auth bypassing admin checks.\n  - `amount` is a decimal string. For Classic paths it's `i64`\n    stroops; for the contract path it's `i128`.\n  - Takes `account` (G-strkey) for the recipient and the asset\n    discriminant above. Returns `{ ok, latestLedger }`.\n- **`fork_etch`** *(new in v0.8.6)* — Foundry's\n  `vm.etch`-equivalent. Hot-swap the WASM under any contract\n  address in one wire call. Takes `contract` (strkey), `wasm`\n  (base64 bytes), optional `liveUntilLedgerSeq`. Internally:\n  install ContractCode, then read-modify-write the contract's\n  instance entry to point at the new code hash. **Storage is\n  preserved verbatim** — if the existing instance carries\n  contract state, swapping code keeps that state intact (the\n  hotfix scenario). **Auto-creates** the instance entry if the\n  target address has none yet — works on any address regardless\n  of prior state, just like Anvil. One wire call replaces the\n  `fork_setCode` + `fork_setStorage` dance the v0.8.5 showcase\n  uses.\n- **`fork_closeLedgers`** *(new in v0.8, renamed from `anvil_mine` in v0.8.1)* —\n  close `ledgers` ledgers (default 1) and bump close-time by\n  `timestampAdvanceSeconds` (default `ledgers * 5` — Stellar's\n  average close rate). Stellar's verb is *closing* a ledger;\n  pushes time-sensitive contract logic (vesting cliffs, oracle\n  staleness) past thresholds without orchestrating real\n  transactions.\n\n### What v0.9.2 server does NOT support\n\nListed up front so nothing surprises you:\n\n- **`getEvents`** — historical event filtering. Diagnostic events\n  emitted during simulation are reachable via `simulateTransaction`'s\n  response.\n- **Ergonomic `fork_*` wrappers** — `setNonce`, `impersonate`.\n  The primitive `fork_setLedgerEntry` covers all of these once\n  the client constructs the right XDR; `fork_setStorage` (v0.8.2),\n  `fork_setCode` (v0.8.3), `fork_setBalance` (v0.8.4 + v0.8.7),\n  `fork_etch` (v0.8.6) are the sugar wrappers landed so far.\n- **`fork_snapshot` / `fork_revert`** — saved-state checkpoints.\n  Scoped to v0.9 (the `Rc\u003cHostImpl\u003e` snapshot model needs its own\n  design pass — either a journaling layer over `RpcSnapshotSource`\n  or a clone-on-snapshot of the entire cache map).\n- **Ledger close as a `sendTransaction` side-effect** — each send\n  applies its writes and bumps the source's `seq_num`, but does\n  *not* automatically advance `env.ledger().sequence_number()`. Use\n  `fork_closeLedgers` (or `env.warp(...)` from lib mode) to push\n  the ledger forward. Auto-close on send is a v0.8.x ergonomic\n  followup.\n- **`resultMetaXdr` on `getTransaction`** — Stellar's\n  `TransactionMeta::V3` carries state-change deltas in a\n  Stellar-core-XDR-heavy shape; v0.6 returns `returnValueXdr` and\n  `appliedChanges` instead. Full meta XDR is a v0.6.x followup.\n\n### Architecture: single-threaded actor\n\naxum HTTP handlers run on a multi-thread tokio runtime; commands flow\nthrough a bounded `mpsc` channel to one OS thread that owns the\n`ForkedEnv`. The SDK's `Env` contains `Rc\u003cHostImpl\u003e` and is `!Send`, so\nit can't live behind `Arc\u003cRwLock\u003e` — single-thread ownership with\nexplicit messaging is the load-bearing constraint of this design.\n\n```\n[HTTP handler 1]──┐\n[HTTP handler 2]──┼──mpsc::channel──→ [worker thread] owns ForkedEnv\n[HTTP handler N]──┘                           │\n                                              └─→ snapshot_source.get()\n                                                  └─→ on cache miss → upstream RPC\n```\n\nCache misses on `getLedgerEntries` block the worker for one upstream\nround-trip. Steady state (after first contact) is local.\n\n### Library API for server mode\n\nIf you'd rather embed the server in your own Rust process (CI test\nharness, custom Stellar tooling), use the library API:\n\n```rust,ignore\nuse soroban_fork::{ForkConfig, server::Server};\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let config = ForkConfig::new(\"https://soroban-rpc.mainnet.stellar.gateway.fm\");\n\n    Server::builder(config)\n        .listen(\"127.0.0.1:8000\".parse().unwrap())\n        .serve()        // runs until SIGINT/SIGTERM\n        .await?;\n\n    Ok(())\n}\n```\n\nFor tests that need to bind ephemeral ports and shut down programmatically:\n\n```rust,ignore\nlet running = Server::builder(config)\n    .listen(\"127.0.0.1:0\".parse().unwrap())   // OS-assigned port\n    .start()\n    .await?;\nlet url = format!(\"http://{}\", running.local_addr());\n// ... drive the server with a real client ...\nrunning.shutdown().await?;\n```\n\n### `RpcSnapshotSource`\n\nThe core primitive. Implements `soroban_env_host::storage::SnapshotSource`:\n\n```rust,ignore\nuse std::sync::Arc;\nuse soroban_fork::{RpcSnapshotSource, RpcConfig};\nuse soroban_fork::RpcClient; // re-exported\n\nlet client = Arc::new(RpcClient::new(\"https://soroban-testnet.stellar.org:443\", RpcConfig::default())?);\nlet source = Arc::new(RpcSnapshotSource::new(client));\nsource.preload(entries);          // pre-load entries from a snapshot file\nlet all_entries = source.entries();  // export for persistence\n```\n\n`RpcSnapshotSource` is `Send + Sync`, so it can be wrapped in `Arc` and\nshared across threads — useful for parallel test runners and the\nupcoming RPC-server mode. Internally the cache stores XDR-encoded bytes\nand parses to `LedgerEntry` only at the SDK boundary, so no `Rc` ever\ncrosses threads.\n\n### Errors\n\nEvery public fallible API returns `Result\u003cT, ForkError\u003e`. The error enum\ndiscriminates transport failures, RPC-level errors, XDR codec failures,\ncache I/O, and protocol-violation cases — no string-typed errors.\n\n### Logging\n\nUses the [`log`](https://docs.rs/log) facade — no output unless a logger\nis initialized in the test binary. Typical setup:\n\n```bash\nRUST_LOG=soroban_fork=info cargo test -- --ignored\n```\n\n## Examples\n\nRunnable demos against live Stellar mainnet. Each one targets a real\ncontract — Blend lending, Phoenix DEX — to show where lazy-fork pays\noff compared to fabricated reserves in a snapshot test.\n\n```sh\n# What does my 50K USDC deposit do to the Blend Fixed pool?\ncargo run --release --example blend_lending\n\n# What's my fill price market-selling 1M XLM into Phoenix?\ncargo run --release --example phoenix_slippage\n\n# Phoenix vs Soroswap on the same XLM/USDC trade — how big is the\n# cross-DEX price gap right now?\ncargo run --release --example cross_dex_arbitrage\n```\n\n`MAINNET_RPC_URL` overrides the upstream RPC. Each example prints\nthe forked ledger sequence and the number of RPC fetches it triggered.\n\nFor server-mode tooling (`@stellar/stellar-sdk`, Stellar Lab,\nFreighter), `examples/server_demo.mjs` shows the JSON-RPC dialect\nworking from Node — no npm install, no XDR dance, just `fetch()`:\n\n```sh\n# shell A — start the fork server\ncargo run --release --features server --bin soroban-fork -- \\\n    serve --rpc https://soroban-rpc.mainnet.stellar.gateway.fm\n\n# shell B — drive it from Node\nnode examples/server_demo.mjs\n```\n\n## Combining with `stellar snapshot create`\n\nFor maximum speed, pre-snapshot known contracts and let `soroban-fork` handle the rest lazily:\n\n```bash\n# Snapshot the main contracts you know about\nstellar snapshot create \\\n  --address $VAULT_CONTRACT \\\n  --network testnet --output json --out vault_state.json\n\nstellar snapshot create \\\n  --address $STRATEGY_CONTRACT \\\n  --network testnet --output json --out strategy_state.json\n\nstellar snapshot merge \\\n  --input vault_state.json --input strategy_state.json \\\n  --output merged.json\n```\n\n```rust\nlet env = ForkConfig::new(\"https://soroban-testnet.stellar.org:443\")\n    .cache_file(\"merged.json\")  // pre-loaded entries skip RPC\n    .build();\n\n// Calls to vault/strategy use cached entries (fast).\n// Calls to USDC token or other dependencies are fetched lazily from RPC.\n```\n\n## Diagnostics\n\nEvery lazy fetch is logged to stderr with human-readable key types:\n\n```\n[soroban-fork] forked at ledger 2070078 (protocol 25)\n[soroban-fork] fetch #1: ContractData(instance)\n[soroban-fork] fetch #2: ContractCode(dee2d494...)\n[soroban-fork] fetch #3: ContractData(persistent)\n[soroban-fork] saved 3 entries to test_cache.json\n```\n\n`env.fetch_count()` returns the total number of RPC calls for programmatic assertions.\n\n## Cache format\n\nThe cache file uses the same JSON format as `stellar snapshot create` (`LedgerSnapshot`). You can:\n- Use a `stellar snapshot create` output as the cache input\n- Share cache files between team members for reproducible tests\n- Inspect cached entries with `stellar xdr decode`\n\nCache is saved automatically when `ForkedEnv` is dropped, including all entries\nthat were lazy-fetched during the test. This means the second run of a test with\n`cache_file` set will be fully local -- zero RPC calls.\n\n## Common pitfalls\n\nReal things that have tripped up integrators wiring soroban-fork into Blend-style\nmainnet tests. Read these before opening an issue.\n\n### `mock_all_auths()` vs `mock_all_auths_allowing_non_root_auth()`\n\nReach for plain **`env.mock_all_auths()`**. Don't use `mock_all_auths_allowing_non_root_auth()` unless\nyou understand exactly which authorisations it disables.\n\nThe `_allowing_non_root_auth` variant skips authorisation checks for *non-root*\n(cross-contract callee) frames. If your contract makes a self-call (`to=self`)\nor relies on `authorize_as_current_contract` declarations, the relaxed mode\n**silently masks** missing auth declarations during fork tests — they pass\nlocally and only fail on testnet with `Error(Auth, InvalidAction)`. By the\ntime you see the testnet error, you've lost the trail back to the missing\ndeclaration.\n\n**Rule of thumb:** start with plain `mock_all_auths()`. Only switch to the\nrelaxed variant when you can name which non-root authorisations you're\nintentionally bypassing — and document why in a comment.\n\nA `ForkConfig::strict_auth(true)` switch that refuses the relaxed variant\non the env was scoped for v0.9.0 but **dropped** when research showed the\nhost's `disable_non_root_auth` flag has no public getter — any\nimplementation we shipped would have been a half-measure with no real\nenforcement. Will revisit if `rs-soroban-env` adds an accessor.\n\n### `include_bytes!(\"…wasm\")` does not rebuild the wasm\n\nIf your test loads a contract via\n`include_bytes!(\"../target/wasm32v1-none/release/my_contract.wasm\")`, Cargo\nwill **not** rebuild the wasm when you edit the contract's `.rs` files.\n`cargo test` rebuilds the test binary, which sees only whatever wasm bytes\nwere on disk the last time `stellar contract build` ran.\n\nYou will see tests pass against stale wasm. The symptom is \"I fixed the\ncontract but the test still observes the old behaviour.\"\n\nThe fix shipped in v0.9.1 is `soroban_fork::workspace_wasm`:\n\n```rust\nlet wasm = soroban_fork::workspace_wasm(\"my_contract\")\n    .expect(\"build my_contract for wasm32v1-none\");\n// Pass `wasm` to whatever needs the bytes — Env::register, an\n// UploadContractWasm envelope, fork_setCode over JSON-RPC, etc.\n```\n\nIt runs `cargo build -p my_contract --target wasm32v1-none --release`\nat test runtime and reads the resulting `.wasm` file. Cargo's\nincremental compilation keeps the rebuild cheap when the source\nhasn't actually changed (sub-second on small crates). Because the\nbuild runs every time you call `workspace_wasm`, the bytes are always\nin sync with the source — the trap closes.\n\nLayout assumption: `my_contract` is a member of the same Cargo\nworkspace as the test, with a `cdylib` target. The workspace root is\nlocated via `cargo metadata`, so `CARGO_TARGET_DIR` overrides are\nhonored automatically. See [`workspace`\nmodule docs](https://docs.rs/soroban-fork/latest/soroban_fork/workspace/index.html)\nfor `workspace_wasm_with(crate_name, target, profile)` if you need to\noverride the default target/profile.\n\nFor test suites that load wasm in many places, the cheaper alternative\nis a `build.rs` in the test crate that runs `cargo build -p \u003ccontract\u003e`\nonce per `cargo test` invocation (and emits `cargo:rerun-if-changed=`\ndirectives on the contract's source files). That keeps the compile\ncost out of every individual test. Pick whichever fits — `workspace_wasm`\nis the smaller diff for most projects.\n\n### `cache_file` for cheap CI\n\nA typical Blend-style mainnet fork test fetches 30–40 ledger entries on the\nfirst run. Without `cache_file`, every CI run pays that round-trip cost — five\ntests × ~35 fetches ≈ 175 live RPC calls per CI build, gated by the upstream\nRPC's rate limits.\n\nSet `.cache_file(\"…\")` once and check the file into git. Subsequent runs (local\nand CI) read entirely from the cache: zero RPC calls, full repro of the\ncaptured state. See [Cache format](#cache-format) above.\n\n```rust\nlet env = ForkConfig::new(rpc_url)\n    .cache_file(\"tests/fixtures/blend_pool_cache.json\")\n    .build()?;\n```\n\n### Debugging cross-contract auth chains\n\nWhen you see `Error(Auth, InvalidAction)` or unexpected `require_auth`\npanics, two complementary tools cover most cases:\n\n- `env.print_auth_tree()` *(new in v0.9.0)* — dumps the recording auth\n  manager's payload set as a Foundry-style indented tree. Names every\n  signer, nonce, contract, function, and arg list that `require_auth`\n  was demanded for during the most recent top-level invocation. See\n  [Auth introspection](#auth-introspection--print_auth_tree) above.\n- `env.print_trace()` (with `.tracing(true)` on the builder) — dumps\n  the cross-contract call tree. The rolled-back frame surfaces which\n  contract refused which authorisation. See\n  [Tracing](#tracing--foundry-style-call-trees) above.\n\nA structured `env.last_auth_failure()` accessor — exact contract,\nfunction, expected authorizer that fired the `InvalidAction` — was\nscoped for v0.9.0 but **dropped** when research showed the host\nconstructs the failure with only the address in its diagnostic args\nand discards the rest. Will revisit if `rs-soroban-env` persists the\nfailure context.\n\n### `into_val(\u0026env)` and `ForkedEnv`\n\n`ForkedEnv` derefs to `\u0026Env`, but the `IntoVal` blanket impls don't follow the\nderef. If you write `something.into_val(\u0026env)` in a test body where\n`env: ForkedEnv`, the compiler will reject it. Either pass `\u0026*env` explicitly,\nor extract the conversion into a helper that takes `env: \u0026Env`. A direct\n`IntoVal\u003cForkedEnv, Val\u003e` impl is being evaluated for v0.9.x.\n\n## Limitations\n\nWhat soroban-fork does NOT yet do — listed up front so nothing surprises you in production:\n\n- ~~**No `sendTransaction` / state mutation through RPC.**~~ *(closed in v0.6.)* Server-mode `sendTransaction` applies writes back to the snapshot source so subsequent reads see them; `getTransaction` retrieves receipts by hash. *(closed in v0.7:)* the fork now mints 10 pre-funded test accounts at build, auto-increments `seq_num` after every successful send, and accepts `UploadContractWasm` + `CreateContract` host functions — full deploy-then-call workflow against forked mainnet works. *(closed in v0.8:)* `fork_setLedgerEntry` and `fork_closeLedgers` extensions land — force-write any `LedgerEntry` and advance the reported ledger directly. Ergonomic `fork_*` wrappers (impersonate / setBalance / setCode / setStorage) are a v0.8.x followup; `fork_snapshot` / `fork_revert` are scoped to v0.9.\n- **No TTL / archival simulation.** Soroban entries carry a `live_until_ledger_seq`; on real mainnet they become archived past that ledger and need a `RestoreFootprint` operation. We track `live_until` in the cache but do not yet model expiry — bumping `env.ledger()` past an entry's `live_until` will not flip it to archived. Tests that depend on TTL-expiry semantics will see false-positives.\n- **No historical state.** `at_ledger(N)` shifts only what `env.ledger().sequence_number()` reports; the actual ledger entries are always fetched at the RPC's *current* latest. Pin to a specific ledger only when paired with `cache_file` for reproducibility, not when expecting historical state.\n- **Tracing renders structure, not metering.** `env.trace()` captures the call tree with decoded args and return values. It does **not** yet render per-frame gas / cost units, contract events, or decoded `HostError` reasons. (Diagnostic events from the host carry call structure but not metering numbers; metering is planned. Server-mode `simulateTransaction` does return real `cost.cpuInsns` separately.)\n- ~~**Server `simulateTransaction` fee fields are stubbed.**~~ *(closed in v0.5.2.)* `minResourceFee` is now derived from the live on-chain fee schedule via `compute_transaction_resource_fee`, and `cost.memBytes` reads `Budget::get_mem_bytes_consumed` directly. Bandwidth + historical-data fees use the actual envelope size received over the wire.\n- **Footprint discovery.** Soroban requires declaring the transaction footprint before execution. The fork tool handles this transparently via the recording-mode footprint in the test environment.\n\n## Requirements\n\n- Rust 1.91+ (the Soroban SDK 25.3.1 floor)\n- `soroban-sdk` 25.x (with `testutils` feature)\n- Network access to a Soroban RPC endpoint\n\n## Why this exists\n\nThe Stellar SDK supports snapshot-based fork testing via `stellar snapshot create` + `Env::from_snapshot_file()`. But you must know every contract address your test will touch in advance. Miss one dependency and the test fails.\n\nThis tool adds the missing piece: **lazy loading on cache miss**. It implements `SnapshotSource` (the trait that feeds ledger entries to the Soroban VM) with an RPC fallback. The standard `soroban_sdk::Env` works unchanged.\n\nSee [stellar/rs-soroban-sdk#1440](https://github.com/stellar/rs-soroban-sdk/issues/1440) for the upstream issue tracking this gap.\n\n## License\n\nMIT OR Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flobotomoe%2Fsoroban-fork","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flobotomoe%2Fsoroban-fork","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flobotomoe%2Fsoroban-fork/lists"}