soroban-fork 0.9.2

Lazy-loading mainnet/testnet fork for Soroban tests. Inspired by Foundry's Anvil.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
# soroban-fork

[![crates.io](https://img.shields.io/crates/v/soroban-fork.svg)](https://crates.io/crates/soroban-fork)
[![docs.rs](https://docs.rs/soroban-fork/badge.svg)](https://docs.rs/soroban-fork)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

Lazy-loading mainnet/testnet fork for Soroban tests. Think [Foundry's Anvil](https://book.getfoundry.sh/anvil/), but for Stellar Soroban.

When 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.

## Install

```toml
[dev-dependencies]
soroban-fork = "0.8"
```

## Usage

```rust,no_run
use soroban_fork::ForkConfig;
use soroban_sdk::{Address, String, Symbol, vec};

#[test]
fn test_against_real_state() {
    let env = ForkConfig::new("https://soroban-testnet.stellar.org:443")
        .cache_file("test_cache.json")   // optional: persist for faster reruns
        .build()
        .expect("fork setup");

    env.mock_all_auths();

    let contract = Address::from_string(&String::from_str(
        &env,
        "CABC...YOUR_CONTRACT_ID",
    ));

    // This lazily fetches the contract's instance, WASM code,
    // and any storage entries from the real network.
    let result: i128 = env.invoke_contract(
        &contract,
        &Symbol::new(&env, "total_assets"),
        vec![&env],
    );

    assert!(result >= 0);

    // env.fetch_count() tells you how many RPC calls were made.
    // Cache is auto-saved on drop (includes lazy-fetched entries).
}
```

## How it works

```
Your test calls contract.total_assets()
         |
         v
  Soroban VM needs a ledger entry
         |
         v
  RpcSnapshotSource.get(key)
         |
    +----+----+
    |         |
  Cache     Cache miss
  hit         |
    |         v
    |    getLedgerEntries RPC call
    |         |
    |         v
    |    Cache result locally
    |         |
    +----+----+
         |
         v
  Return entry to VM
```

- **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).
- **Subsequent runs**: if `cache_file` is set, entries are loaded from disk. Only new entries trigger RPC calls.
- **State changes are local**: the real network is never modified. Deposits, transfers, and other mutations happen in memory only.

## API

### `ForkConfig`

```rust
ForkConfig::new(rpc_url)           // Soroban RPC endpoint
    .cache_file("cache.json")             // optional: disk persistence + auto-save on drop
    .network_id(bytes)                    // optional: override the SHA-256 network id
    .fetch_mode(FetchMode::Strict)        // optional: Strict (default) or Lenient
    .at_ledger(1_234_567)                 // optional: pin the Env's reported sequence
    .pinned_timestamp(1_700_000_000)      // optional: pin the Env's close time
    .max_protocol_version(25)             // optional: cap the protocol the VM reports
    .tracing(true)                        // optional: capture cross-contract call tree
    .rpc_config(RpcConfig { retries: 5, ..RpcConfig::default() })
    .build()?                             // returns Result<ForkedEnv, ForkError>
```

Network metadata (passphrase + SHA-256 id) is fetched from the RPC's
`getNetwork` method at build time — no URL heuristics, no silent defaults.
Override with `.network_id(bytes)` only if you actually need to.

The `Env`'s reported timestamp defaults to the close time of the latest
ledger, fetched via `getLedgers` at build time. Tests are reproducible
across runs out of the box — pin an explicit value via
`.pinned_timestamp(...)` only when you need to anchor to a specific
moment (e.g. reproducing a known historical scenario).

### `ForkedEnv`

Returned by `ForkConfig::build()`. Implements `Deref<Target = Env>` so all SDK
methods work transparently. Adds fork-specific capabilities:

```rust,ignore
let env = ForkConfig::new(rpc_url).cache_file("cache.json").build()?;

// Use like a regular Env (via Deref)
env.mock_all_auths();
let result: i128 = env.invoke_contract(&addr, &symbol, vec![&env]);

// Fork-specific methods
env.fetch_count();                 // number of RPC calls made
env.save_cache()?;                 // explicit save (also called automatically on drop)
env.warp_time(86_400);             // advance ledger timestamp + sequence
env.deal_token(&usdc, &who, amt);  // Foundry-style balance deal
env.env();                         // &Env (for edge cases where Deref doesn't suffice)
```

### `FetchMode`

Controls behavior when the RPC fails from inside the VM loop (where the
`SnapshotSource` trait can't return a typed error):

- **`Strict`** (default): panic. Best for tests — a fetch failure means the test setup is wrong, and you want the stack trace.
- **`Lenient`**: log at `warn!` level and return `None`. Useful when partial state is acceptable.

### `RpcConfig`

Transport tunables. Defaults: 3 retries with 300 ms exponential backoff
plus full jitter (so concurrent test runners don't synchronise their
retries into a thundering herd), 30 s per-request timeout, 200-key batch
size (Soroban RPC cap). Customize via `.rpc_config(RpcConfig { .. })` on
the builder. HTTP 408, 425, 429, and 5xx responses are retried; other
4xx codes fail fast and include the response body for diagnostics.

### Tracing — Foundry-style call trees

Set `.tracing(true)` on the builder to capture cross-contract call trees.
The host runs in `DiagnosticLevel::Debug`, every `fn_call`/`fn_return`
emits a diagnostic event, and `env.trace()` reconstructs the tree:

```rust
let env = ForkConfig::new(rpc_url)
    .tracing(true)
    .build()?;

env.invoke_contract::<i128>(&vault, &Symbol::new(&env, "deposit"), args);
env.print_trace();
```

```text
[TRACE]
  [CABC…XYZ1] deposit(GACC…QRST, 1000000)
    [CCDE…UVW2] transfer_from(GACC…QRST, CABC…XYZ1, 1000000)
      ← ()
    [CFGH…IJK3] invest(1000000)
      ← 1010000
    ← 1010000
```

Programmatic access via `env.trace()` returns a `Trace` with structured
`TraceFrame`s — useful for asserting call structure or balances inside
a test. Failed calls render as `[rolled back]`; WASM traps show as
`TRAPPED (no fn_return)`.

**Per-invocation scoping.** The host's `InvocationMeter` clears the
events buffer at the start of every top-level `invoke_contract`, so each
`trace()` reflects only the most recent top-level call. Capture before
the next call if you need history. See the
[`trace` module docs](https://docs.rs/soroban-fork/latest/soroban_fork/trace/index.html)
for wire-format details and caveats (single-`Vec`-arg ambiguity).

### Auth introspection — `print_auth_tree`

When debugging cross-contract authorization (`Error(Auth, InvalidAction)`,
unexpected `require_auth` panics, "did Alice actually have to sign this?"),
read out the recording auth manager's payload set:

```rust
let env = ForkConfig::new(rpc_url).build()?;
env.mock_all_auths();

env.invoke_contract::<()>(&usdc, &Symbol::new(&env, "transfer"), args);

env.print_auth_tree();
```

```text
[AUTH]
  payload #0  signer=GA62…J3CT  nonce=5541220902715666415
    [CCW6…MI75] transfer(GA62…J3CT, GB7O…7AMM, 250000000)
```

Programmatic access via `env.auth_tree()` returns an
[`AuthTree`](https://docs.rs/soroban-fork/latest/soroban_fork/auth_tree/struct.AuthTree.html)
with `payload_count()` / `invocation_count()` / `is_empty()` accessors —
useful for asserting that a multi-hop call demanded exactly the expected
number of `require_auth`s. Raw `Vec<RecordedAuthPayload>` is available
via `env.auth_payloads()`.

**Per-invocation scoping**, like `trace()`: only payloads from the most
recent top-level `invoke_contract` are visible.

**Limits inherited from the upstream host crate**, documented honestly:

- `Error(Auth, InvalidAction)` carries only the address — the failed
  contract / function / expected authorizer are constructed locally
  inside the host and not persisted to any accessor we can read out.
  After a failed call, `auth_tree()` reflects whatever payload set the
  host left in its `previous_authorization_manager`; the exact contents
  after a panic mid-invocation are an implementation detail of
  `soroban-env-host`. A structured `last_auth_failure()` awaits an
  upstream change.
- Whether `mock_all_auths_allowing_non_root_auth` was used is not
  exposed by the host. The README's "Common pitfalls" section documents
  the trap; an enforceable `strict_auth` mode awaits an upstream change.

## JSON-RPC server mode

Library mode (everything above) is for Rust tests. **Server mode** turns
soroban-fork into a Stellar Soroban RPC drop-in that any tooling — JS,
Python, Go SDKs, Stellar Lab, Freighter, custom clients — can point at:

```sh
cargo install soroban-fork --features server
soroban-fork serve --rpc https://soroban-rpc.mainnet.stellar.gateway.fm
# → serving JSON-RPC on http://127.0.0.1:8000
```

Then any client speaking the Stellar RPC dialect:

```js
import { SorobanRpc } from "@stellar/stellar-sdk";
const server = new SorobanRpc.Server("http://localhost:8000");
const account = await server.getAccount("GA5...");
const result = await server.simulateTransaction(tx);  // hits the fork
```

Or via the Rust `stellar-rpc-client`, raw curl, or anything that
understands the spec.

### Pre-funded test accounts *(new in v0.7)*

The fork mints **10 deterministic test accounts** at build time, each
with 100K XLM and a USDC trustline ready to receive. Same seed
produces the same accounts every run, so test code can hard-code
addresses by index. The CLI prints them on startup:

```
soroban-fork v0.7
Listening on http://127.0.0.1:8000

Available test accounts:
(0) GBXXX...AB12  (100000.0000000 XLM)  ->  SAXXX...CD34
(1) GCYYY...EF56  (100000.0000000 XLM)  ->  SAXXX...GH78
...
```

Pass them to JS-SDK's `Keypair.fromSecret(...)` to sign envelopes.
After every successful `sendTransaction`, the source account's
sequence number auto-increments — so chained `getAccount` →
`TransactionBuilder` → `sendTransaction` loops just work.

**Real DEX flow works end-to-end.** A test account can swap XLM →
USDC against the live Phoenix DEX (or Soroswap, Aquarius, …) and
the USDC actually lands in its trustline. Smoke-tested:
1000 XLM → 167.4020548 USDC at the live mainnet pool reserves.

**No hidden hardcode.** The trustline default targets the mainnet
USDC issuer (Circle); for testnet, futurenet, or a custom fork,
override via `ForkConfig::test_account_trustlines(vec![...])`. The
trustlines are written with `flags = AUTHORIZED_FLAG`, `limit = i64::MAX` —
shape-equivalent to running `ChangeTrust` then having the issuer
authorize, just bootstrapped at build time. Auth runs in trust mode
(`Recording(false)`) so unsigned envelopes from test code apply
without ceremony.

Override count via `--accounts N` (set to `0` to disable). For
library users, `ForkConfig::test_account_count(n).build()` exposes
the same machinery; read accounts back with `env.test_accounts()`.

### Deploy your own contracts onto the fork *(new in v0.7)*

The same `sendTransaction` accepts `HostFunction::UploadContractWasm`
and `HostFunction::CreateContract`, so you can deploy custom contracts
straight onto the forked mainnet state and have them call live
production contracts. The test suite's
`server_deploy_and_invoke_custom_contract` covers the full loop:

1. Upload a tiny `add(i32, i32) -> i32` WASM
2. Create the contract instance from the uploaded hash
3. Invoke `add(2, 3)` on the deployed contract — returns 5

Cross-protocol scenarios (your contract calls Blend, Phoenix,
Soroswap, etc.) follow the same pattern: dependencies the deployed
contract reaches into get lazy-fetched from mainnet and cached
locally.

### The headline showcase: cheatcode-only deploy

What makes the toolset matter, in one test:

1. **`fork_setCode`** installs your WASM bytes (no `UploadContractWasm` envelope)
2. **`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)
3. **`simulateTransaction`** invokes your cheatcode-deployed contract, returns the result
4. 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

Two 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.

### Methods supported in v0.9.2

- **`getHealth`** — fork status + latest ledger
- **`getVersionInfo`** — server version + protocol version
- **`getNetwork`** — passphrase + protocol version + network ID (proxied
  from the upstream RPC at fork-build time, then served locally)
- **`getLatestLedger`** — fork's reported ledger sequence + protocol
- **`getLedgers`** — single-element page describing the fork point with
  real `ledgerCloseTime` (Unix-seconds string, per Stellar convention)
- **`getLedgerEntries`** — base64-XDR `LedgerKey` array → array of
  entries; routed through the fork's lazy-fetch cache, so first hit
  proxies upstream and subsequent hits are local
- **`simulateTransaction`** — accepts a base64-XDR `TransactionEnvelope`
  with one `InvokeHostFunctionOp`, runs it via the host's recording-mode
  primitive, returns:
  - `results[0].xdr` — the function's return value (`ScVal`)
  - `results[0].auth` — auth entries `sendTransaction` would need
  - `transactionData` — `SorobanTransactionData` with recorded footprint
    and `resourceFee` matching `minResourceFee`
  - `events` — diagnostic events emitted during simulation
  - `cost.cpuInsns` / `cost.memBytes` — real numbers from the host's
    `Budget`, *not* a `write_bytes` proxy
  - `minResourceFee` — derived from the live on-chain Soroban fee
    schedule via `compute_transaction_resource_fee` (since v0.5.2)
  - `latestLedger` — fork's reported ledger
- **`sendTransaction`** *(new in v0.6)* — applies the host invocation's
  writes back to the snapshot source so subsequent reads see them.
  Auth runs in trust mode (`Recording(false)`) so unsigned envelopes
  from test code apply without ceremony. Returns `status`
  (`"SUCCESS"` / `"ERROR"`), `hash` (sha256 of the envelope),
  `appliedChanges` (number of `LedgerEntryChange`s written), and
  the original envelope echo.
- **`getTransaction`** *(new in v0.6)* — receipt lookup by hash.
  Returns `"SUCCESS"` / `"FAILED"` / `"NOT_FOUND"`, plus the original
  envelope, the host function's `ScVal` return value, and the
  applied-changes count when found.

#### Fork-mode extensions (`fork_*`)

Non-standard methods, only available against soroban-fork. The
`fork_` prefix marks the namespace boundary explicitly so a
client can distinguish "this works against any Stellar RPC" from
"this only works against the fork."

- **`fork_setLedgerEntry`** *(new in v0.8, renamed from `anvil_setLedgerEntry` in v0.8.1)* —
  force-write a base64-XDR `LedgerEntry` to any `LedgerKey`
  directly in the snapshot source, bypassing host-level checks.
  Load-bearing primitive for stress-test scenarios — oracle price
  manipulation, force-set token balances, replace contract code,
  all reduce to this one entry write.
- **`fork_setStorage`** *(new in v0.8.2)* — sugar over
  `fork_setLedgerEntry` for the common case of writing into a
  contract's storage. Takes `contract` (strkey), `key` (base64
  ScVal), `value` (base64 ScVal), optional `durability`
  (`"persistent"` (default) / `"temporary"`), and optional
  `liveUntilLedgerSeq`. The handler builds the `ContractData`
  XDR server-side so clients don't have to assemble the
  multi-level enum nesting themselves. Use this for oracle
  price overrides and contract-storage scenarios.
- **`fork_setCode`** *(new in v0.8.3)* — upload WASM bytes as a
  `ContractCode` ledger entry, keyed by sha256 of those bytes.
  Takes `wasm` (base64) and optional `liveUntilLedgerSeq`.
  Returns `{ ok, hash, latestLedger }` — the hash is server-derived
  (the host computes it the same way), so callers can wire a
  follow-up `CreateContract` (or `fork_setStorage` over a
  `ContractInstance` ScVal) to point at the uploaded code without
  any host invocation.
- **`fork_setBalance`** *(new in v0.8.4, Soroban-token path added in v0.8.7)* —
  Foundry's `deal()`-equivalent for Stellar. Three asset shapes:
  - `"native"` (default) — XLM, balance lives on `AccountEntry`.
    Auto-creates the account with master threshold 1 if missing.
  - `{ code, issuer }` — Classic credit asset (USDC, EURC, …),
    balance lives on `TrustLineEntry`. Auto-creates the trustline
    with `flags = AUTHORIZED`, `limit = i64::MAX` — equivalent to
    having run `ChangeTrust` and the issuer authorising.
  - `{ contract }` *(v0.8.7)* — any SEP-41-shaped Soroban token
    (the SAC for Classic assets, custom Soroban tokens like BLND).
    Handler simulates `balance(to)`, computes the delta, and
    invokes `mint(to, delta)` or `burn(to, |delta|)` with
    trust-mode auth bypassing admin checks.
  - `amount` is a decimal string. For Classic paths it's `i64`
    stroops; for the contract path it's `i128`.
  - Takes `account` (G-strkey) for the recipient and the asset
    discriminant above. Returns `{ ok, latestLedger }`.
- **`fork_etch`** *(new in v0.8.6)* — Foundry's
  `vm.etch`-equivalent. Hot-swap the WASM under any contract
  address in one wire call. Takes `contract` (strkey), `wasm`
  (base64 bytes), optional `liveUntilLedgerSeq`. Internally:
  install ContractCode, then read-modify-write the contract's
  instance entry to point at the new code hash. **Storage is
  preserved verbatim** — if the existing instance carries
  contract state, swapping code keeps that state intact (the
  hotfix scenario). **Auto-creates** the instance entry if the
  target address has none yet — works on any address regardless
  of prior state, just like Anvil. One wire call replaces the
  `fork_setCode` + `fork_setStorage` dance the v0.8.5 showcase
  uses.
- **`fork_closeLedgers`** *(new in v0.8, renamed from `anvil_mine` in v0.8.1)* —
  close `ledgers` ledgers (default 1) and bump close-time by
  `timestampAdvanceSeconds` (default `ledgers * 5` — Stellar's
  average close rate). Stellar's verb is *closing* a ledger;
  pushes time-sensitive contract logic (vesting cliffs, oracle
  staleness) past thresholds without orchestrating real
  transactions.

### What v0.9.2 server does NOT support

Listed up front so nothing surprises you:

- **`getEvents`** — historical event filtering. Diagnostic events
  emitted during simulation are reachable via `simulateTransaction`'s
  response.
- **Ergonomic `fork_*` wrappers** — `setNonce`, `impersonate`.
  The primitive `fork_setLedgerEntry` covers all of these once
  the client constructs the right XDR; `fork_setStorage` (v0.8.2),
  `fork_setCode` (v0.8.3), `fork_setBalance` (v0.8.4 + v0.8.7),
  `fork_etch` (v0.8.6) are the sugar wrappers landed so far.
- **`fork_snapshot` / `fork_revert`** — saved-state checkpoints.
  Scoped to v0.9 (the `Rc<HostImpl>` snapshot model needs its own
  design pass — either a journaling layer over `RpcSnapshotSource`
  or a clone-on-snapshot of the entire cache map).
- **Ledger close as a `sendTransaction` side-effect** — each send
  applies its writes and bumps the source's `seq_num`, but does
  *not* automatically advance `env.ledger().sequence_number()`. Use
  `fork_closeLedgers` (or `env.warp(...)` from lib mode) to push
  the ledger forward. Auto-close on send is a v0.8.x ergonomic
  followup.
- **`resultMetaXdr` on `getTransaction`** — Stellar's
  `TransactionMeta::V3` carries state-change deltas in a
  Stellar-core-XDR-heavy shape; v0.6 returns `returnValueXdr` and
  `appliedChanges` instead. Full meta XDR is a v0.6.x followup.

### Architecture: single-threaded actor

axum HTTP handlers run on a multi-thread tokio runtime; commands flow
through a bounded `mpsc` channel to one OS thread that owns the
`ForkedEnv`. The SDK's `Env` contains `Rc<HostImpl>` and is `!Send`, so
it can't live behind `Arc<RwLock>` — single-thread ownership with
explicit messaging is the load-bearing constraint of this design.

```
[HTTP handler 1]──┐
[HTTP handler 2]──┼──mpsc::channel──→ [worker thread] owns ForkedEnv
[HTTP handler N]──┘                           │
                                              └─→ snapshot_source.get()
                                                  └─→ on cache miss → upstream RPC
```

Cache misses on `getLedgerEntries` block the worker for one upstream
round-trip. Steady state (after first contact) is local.

### Library API for server mode

If you'd rather embed the server in your own Rust process (CI test
harness, custom Stellar tooling), use the library API:

```rust,ignore
use soroban_fork::{ForkConfig, server::Server};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ForkConfig::new("https://soroban-rpc.mainnet.stellar.gateway.fm");

    Server::builder(config)
        .listen("127.0.0.1:8000".parse().unwrap())
        .serve()        // runs until SIGINT/SIGTERM
        .await?;

    Ok(())
}
```

For tests that need to bind ephemeral ports and shut down programmatically:

```rust,ignore
let running = Server::builder(config)
    .listen("127.0.0.1:0".parse().unwrap())   // OS-assigned port
    .start()
    .await?;
let url = format!("http://{}", running.local_addr());
// ... drive the server with a real client ...
running.shutdown().await?;
```

### `RpcSnapshotSource`

The core primitive. Implements `soroban_env_host::storage::SnapshotSource`:

```rust,ignore
use std::sync::Arc;
use soroban_fork::{RpcSnapshotSource, RpcConfig};
use soroban_fork::RpcClient; // re-exported

let client = Arc::new(RpcClient::new("https://soroban-testnet.stellar.org:443", RpcConfig::default())?);
let source = Arc::new(RpcSnapshotSource::new(client));
source.preload(entries);          // pre-load entries from a snapshot file
let all_entries = source.entries();  // export for persistence
```

`RpcSnapshotSource` is `Send + Sync`, so it can be wrapped in `Arc` and
shared across threads — useful for parallel test runners and the
upcoming RPC-server mode. Internally the cache stores XDR-encoded bytes
and parses to `LedgerEntry` only at the SDK boundary, so no `Rc` ever
crosses threads.

### Errors

Every public fallible API returns `Result<T, ForkError>`. The error enum
discriminates transport failures, RPC-level errors, XDR codec failures,
cache I/O, and protocol-violation cases — no string-typed errors.

### Logging

Uses the [`log`](https://docs.rs/log) facade — no output unless a logger
is initialized in the test binary. Typical setup:

```bash
RUST_LOG=soroban_fork=info cargo test -- --ignored
```

## Examples

Runnable demos against live Stellar mainnet. Each one targets a real
contract — Blend lending, Phoenix DEX — to show where lazy-fork pays
off compared to fabricated reserves in a snapshot test.

```sh
# What does my 50K USDC deposit do to the Blend Fixed pool?
cargo run --release --example blend_lending

# What's my fill price market-selling 1M XLM into Phoenix?
cargo run --release --example phoenix_slippage

# Phoenix vs Soroswap on the same XLM/USDC trade — how big is the
# cross-DEX price gap right now?
cargo run --release --example cross_dex_arbitrage
```

`MAINNET_RPC_URL` overrides the upstream RPC. Each example prints
the forked ledger sequence and the number of RPC fetches it triggered.

For server-mode tooling (`@stellar/stellar-sdk`, Stellar Lab,
Freighter), `examples/server_demo.mjs` shows the JSON-RPC dialect
working from Node — no npm install, no XDR dance, just `fetch()`:

```sh
# shell A — start the fork server
cargo run --release --features server --bin soroban-fork -- \
    serve --rpc https://soroban-rpc.mainnet.stellar.gateway.fm

# shell B — drive it from Node
node examples/server_demo.mjs
```

## Combining with `stellar snapshot create`

For maximum speed, pre-snapshot known contracts and let `soroban-fork` handle the rest lazily:

```bash
# Snapshot the main contracts you know about
stellar snapshot create \
  --address $VAULT_CONTRACT \
  --network testnet --output json --out vault_state.json

stellar snapshot create \
  --address $STRATEGY_CONTRACT \
  --network testnet --output json --out strategy_state.json

stellar snapshot merge \
  --input vault_state.json --input strategy_state.json \
  --output merged.json
```

```rust
let env = ForkConfig::new("https://soroban-testnet.stellar.org:443")
    .cache_file("merged.json")  // pre-loaded entries skip RPC
    .build();

// Calls to vault/strategy use cached entries (fast).
// Calls to USDC token or other dependencies are fetched lazily from RPC.
```

## Diagnostics

Every lazy fetch is logged to stderr with human-readable key types:

```
[soroban-fork] forked at ledger 2070078 (protocol 25)
[soroban-fork] fetch #1: ContractData(instance)
[soroban-fork] fetch #2: ContractCode(dee2d494...)
[soroban-fork] fetch #3: ContractData(persistent)
[soroban-fork] saved 3 entries to test_cache.json
```

`env.fetch_count()` returns the total number of RPC calls for programmatic assertions.

## Cache format

The cache file uses the same JSON format as `stellar snapshot create` (`LedgerSnapshot`). You can:
- Use a `stellar snapshot create` output as the cache input
- Share cache files between team members for reproducible tests
- Inspect cached entries with `stellar xdr decode`

Cache is saved automatically when `ForkedEnv` is dropped, including all entries
that were lazy-fetched during the test. This means the second run of a test with
`cache_file` set will be fully local -- zero RPC calls.

## Common pitfalls

Real things that have tripped up integrators wiring soroban-fork into Blend-style
mainnet tests. Read these before opening an issue.

### `mock_all_auths()` vs `mock_all_auths_allowing_non_root_auth()`

Reach for plain **`env.mock_all_auths()`**. Don't use `mock_all_auths_allowing_non_root_auth()` unless
you understand exactly which authorisations it disables.

The `_allowing_non_root_auth` variant skips authorisation checks for *non-root*
(cross-contract callee) frames. If your contract makes a self-call (`to=self`)
or relies on `authorize_as_current_contract` declarations, the relaxed mode
**silently masks** missing auth declarations during fork tests — they pass
locally and only fail on testnet with `Error(Auth, InvalidAction)`. By the
time you see the testnet error, you've lost the trail back to the missing
declaration.

**Rule of thumb:** start with plain `mock_all_auths()`. Only switch to the
relaxed variant when you can name which non-root authorisations you're
intentionally bypassing — and document why in a comment.

A `ForkConfig::strict_auth(true)` switch that refuses the relaxed variant
on the env was scoped for v0.9.0 but **dropped** when research showed the
host's `disable_non_root_auth` flag has no public getter — any
implementation we shipped would have been a half-measure with no real
enforcement. Will revisit if `rs-soroban-env` adds an accessor.

### `include_bytes!("…wasm")` does not rebuild the wasm

If your test loads a contract via
`include_bytes!("../target/wasm32v1-none/release/my_contract.wasm")`, Cargo
will **not** rebuild the wasm when you edit the contract's `.rs` files.
`cargo test` rebuilds the test binary, which sees only whatever wasm bytes
were on disk the last time `stellar contract build` ran.

You will see tests pass against stale wasm. The symptom is "I fixed the
contract but the test still observes the old behaviour."

The fix shipped in v0.9.1 is `soroban_fork::workspace_wasm`:

```rust
let wasm = soroban_fork::workspace_wasm("my_contract")
    .expect("build my_contract for wasm32v1-none");
// Pass `wasm` to whatever needs the bytes — Env::register, an
// UploadContractWasm envelope, fork_setCode over JSON-RPC, etc.
```

It runs `cargo build -p my_contract --target wasm32v1-none --release`
at test runtime and reads the resulting `.wasm` file. Cargo's
incremental compilation keeps the rebuild cheap when the source
hasn't actually changed (sub-second on small crates). Because the
build runs every time you call `workspace_wasm`, the bytes are always
in sync with the source — the trap closes.

Layout assumption: `my_contract` is a member of the same Cargo
workspace as the test, with a `cdylib` target. The workspace root is
located via `cargo metadata`, so `CARGO_TARGET_DIR` overrides are
honored automatically. See [`workspace`
module docs](https://docs.rs/soroban-fork/latest/soroban_fork/workspace/index.html)
for `workspace_wasm_with(crate_name, target, profile)` if you need to
override the default target/profile.

For test suites that load wasm in many places, the cheaper alternative
is a `build.rs` in the test crate that runs `cargo build -p <contract>`
once per `cargo test` invocation (and emits `cargo:rerun-if-changed=`
directives on the contract's source files). That keeps the compile
cost out of every individual test. Pick whichever fits — `workspace_wasm`
is the smaller diff for most projects.

### `cache_file` for cheap CI

A typical Blend-style mainnet fork test fetches 30–40 ledger entries on the
first run. Without `cache_file`, every CI run pays that round-trip cost — five
tests × ~35 fetches ≈ 175 live RPC calls per CI build, gated by the upstream
RPC's rate limits.

Set `.cache_file("…")` once and check the file into git. Subsequent runs (local
and CI) read entirely from the cache: zero RPC calls, full repro of the
captured state. See [Cache format](#cache-format) above.

```rust
let env = ForkConfig::new(rpc_url)
    .cache_file("tests/fixtures/blend_pool_cache.json")
    .build()?;
```

### Debugging cross-contract auth chains

When you see `Error(Auth, InvalidAction)` or unexpected `require_auth`
panics, two complementary tools cover most cases:

- `env.print_auth_tree()` *(new in v0.9.0)* — dumps the recording auth
  manager's payload set as a Foundry-style indented tree. Names every
  signer, nonce, contract, function, and arg list that `require_auth`
  was demanded for during the most recent top-level invocation. See
  [Auth introspection](#auth-introspection--print_auth_tree) above.
- `env.print_trace()` (with `.tracing(true)` on the builder) — dumps
  the cross-contract call tree. The rolled-back frame surfaces which
  contract refused which authorisation. See
  [Tracing](#tracing--foundry-style-call-trees) above.

A structured `env.last_auth_failure()` accessor — exact contract,
function, expected authorizer that fired the `InvalidAction` — was
scoped for v0.9.0 but **dropped** when research showed the host
constructs the failure with only the address in its diagnostic args
and discards the rest. Will revisit if `rs-soroban-env` persists the
failure context.

### `into_val(&env)` and `ForkedEnv`

`ForkedEnv` derefs to `&Env`, but the `IntoVal` blanket impls don't follow the
deref. If you write `something.into_val(&env)` in a test body where
`env: ForkedEnv`, the compiler will reject it. Either pass `&*env` explicitly,
or extract the conversion into a helper that takes `env: &Env`. A direct
`IntoVal<ForkedEnv, Val>` impl is being evaluated for v0.9.x.

## Limitations

What soroban-fork does NOT yet do — listed up front so nothing surprises you in production:

- ~~**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.
- **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.
- **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.
- **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.)
- ~~**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.
- **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.

## Requirements

- Rust 1.91+ (the Soroban SDK 25.3.1 floor)
- `soroban-sdk` 25.x (with `testutils` feature)
- Network access to a Soroban RPC endpoint

## Why this exists

The 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.

This 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.

See [stellar/rs-soroban-sdk#1440](https://github.com/stellar/rs-soroban-sdk/issues/1440) for the upstream issue tracking this gap.

## License

MIT OR Apache-2.0