Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ reports/
state.kore
.claude/
test/fixtures/sample-contract/target/
test/fixtures/*/target/
examples/*/target/
70 changes: 50 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,41 +51,71 @@ at all — so you can see the debugger working in seconds. See

## Usage

Add a `soroban` configuration to your `.vscode/launch.json`. The common case is:
build a contract, run a function on a local network, and debug the result.
Add a `soroban` configuration to your `.vscode/launch.json`. A configuration describes an ordered sequence of transactions run against one fresh local ledger, and names which transaction to trace and debug — the last one by default. This lets you set up whatever state the call under test depends on (deploy other contracts, run a constructor, seed storage) before the transaction you actually want to step through.

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Debug add(1, 2)",
"contract": "${workspaceFolder}", // crate dir containing Cargo.toml
"function": "add",
"args": [
{ "value": 1, "type": "u32" },
{ "value": 2, "type": "u32" }
]
"name": "Debug supply",
"transactions": [
// deploy: build a crate dir (or point `wasm` at a prebuilt .wasm) and
// register it under a handle `id` that later invokes reference.
{ "kind": "deploy", "id": "pool", "contract": "${workspaceFolder}" },

// invoke: call a function on a deployed handle. `args` is an object keyed
// by the function's parameter names.
{ "kind": "invoke", "contract": "pool", "function": "__constructor",
"args": { "admin": "${sourceAddress}" } },
{ "kind": "invoke", "contract": "pool", "function": "supply",
"args": { "requests": [[{ "tag": "Native" }, "1000"]] } }
],
"trace": "last"
}
```

Set a breakpoint in your contract's Rust source, start the configuration, and
step through — forward or backward.
Set a breakpoint in your contract's Rust source, start the configuration, and step through the traced transaction — forward or backward.

Even the simplest single-call session is one `deploy` plus one `invoke` — there is one config shape, so a two-contract system is just a longer `transactions` array. See [`docs/debug-config.md`](docs/debug-config.md) for the complete reference, including multi-contract systems, composite argument types, and offline replay.

### Configuration reference

Top-level attributes:

| Attribute | Description |
|-----------|-------------|
| `function` *(required)* | Name of the contract function to invoke and debug. |
| `args` | Function arguments, each `{ "value": …, "type": "u32" \| "i128" \| "symbol" \| "address" \| … }`. |
| `contract` | Path to the contract crate directory (with `Cargo.toml`). Defaults to `${workspaceFolder}`. |
| `wasmPath` | Path to a prebuilt `.wasm`. Overrides building from `contract`. |
| `debugInfo` | Build with debug info for Rust source mapping (default `true`; set `false` to debug at the wasm level only). |
| `rawTrace` | Replay a previously recorded run from a file instead of building and deploying. |
| `transactions` | Ordered, non-empty array of `deploy` / `invoke` steps (see below) — the live sequence to run. |
| `trace` | Which transaction feeds the debug session: `"last"` (default), a 0-based index into `transactions`, or a step `id` (a deploy's `id` or an invoke's optional `id`). |
| `sourceSecret` | Source account secret (`S…`) used to sign every transaction. A deterministic account is derived if omitted. Its address is available in `args` as `${sourceAddress}`. |
| `node` | Local-network connection/spawn settings: `attach`, `host`, `port`, `command`, `ioDir`. |
| `sourceSecret` | Optional source account secret (`S…`). A fresh account is used if omitted. |
| `rawTrace` | Replay a previously recorded run from a file instead of building and deploying (optionally with `wasmPath` for source mapping). |

A **`deploy`** step uploads a contract and registers a handle:

| Field | Description |
|-------|-------------|
| `kind` *(required)* | `"deploy"`. |
| `id` *(required)* | Handle name that later `invoke` steps reference via their `contract` field, and that `trace` can select. |
| `contract` | Path to the contract crate directory (with `Cargo.toml`) to build. |
| `wasm` | Path to a prebuilt `.wasm`. Overrides building from `contract`; one of `contract` / `wasm` is required. |
| `buildCommand` | Command used to build a `contract` dir (default `stellar contract build`). |
| `debugInfo` | Build with debug info for Rust source mapping (default `true`; set `false` to debug at the wasm level only). |

An **`invoke`** step calls a function on a deployed handle:

| Field | Description |
|-------|-------------|
| `kind` *(required)* | `"invoke"`. |
| `contract` *(required)* | Handle `id` of an earlier `deploy` step. |
| `function` *(required)* | Name of the contract function to call. |
| `args` | Arguments, as an object keyed by the function's parameter names. Values follow the contract's own spec, so composites work: an enum is `{ "tag": "Native" }` or `{ "tag": "Other", "values": [7] }`, a tuple or vec is a JSON array, an `i128` is a decimal string, an address is a `G…`/`C…` string. |
| `id` | Optional label so `trace` can select this invoke's transaction. |

Two substitution tokens are expanded inside string `args` values: `${sourceAddress}` (the source account's address) and `${contract:<id>}` (the deployed address behind a handle).

`args` also accepts a lower-level positional form — an array `[{ "type": "u32", "value": 1 }, …]`, one entry per parameter — as an alternative to the named object.

Two settings let you point at executables that aren't on your `PATH`:
`soroban.stellar.path` and `soroban.kometNode.path`.
Two settings let you point at executables that aren't on your `PATH`: `soroban.stellar.path` and `soroban.kometNode.path`. For the full reference — multi-contract systems, every argument shape, and offline replay — see [`docs/debug-config.md`](docs/debug-config.md).

### Beyond the editor

Expand Down
263 changes: 263 additions & 0 deletions docs/debug-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Debug configuration reference

> **Audience:** `soroban developer` · `getting started` · `writing launch.json`
>
> **TL;DR:** A `soroban` launch configuration describes an ordered sequence of
> transactions run against one fresh local ledger, and names which transaction
> to trace and debug. You set up whatever state your call depends on (deploy
> other contracts, run a constructor, seed storage) as earlier transactions,
> then point `trace` at the one you want to step through. This document covers
> every field, the argument encodings (including enums, tuples, structs, and
> addresses), the `${…}` substitution tokens, and offline replay — with worked
> examples. For the editor tour see [`../examples/README.md`](../examples/README.md);
> for the CLIs see [`trace-cli.md`](./trace-cli.md) and [`dap-cli.md`](./dap-cli.md).

## Two modes

A launch configuration runs in one of two modes:

- **Live** — the default. You give a `transactions` array; the debugger spawns
(or attaches to) a local komet-node, runs the whole sequence against one fresh
ledger, and drops you into a debug session on the transaction named by
`trace`.
- **Replay** — you give a `rawTrace` file. The debugger replays a previously
recorded execution with no network and no toolchain. Optionally pair it with a
`wasmPath` for Rust source mapping. See [Replay mode](#replay-mode).

A configuration is one JSON object in your `.vscode/launch.json` under
`configurations`. Every live configuration shares this skeleton:

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "…", // shown in the Run and Debug dropdown
"transactions": [ … ], // the ordered sequence (required)
"trace": "last" // which transaction to debug (optional, default "last")
}
```

## The `transactions` sequence

`transactions` is an ordered, non-empty array of **steps**. Each step is either
a `deploy` (upload a contract and register a handle) or an `invoke` (call a
function on a deployed handle). Steps run in order against one accumulating
ledger, so state written by one step is visible to later steps.

```jsonc
"transactions": [
{ "kind": "deploy", "id": "pool", "contract": "${workspaceFolder}" },
{ "kind": "invoke", "contract": "pool", "function": "__constructor",
"args": { "admin": "${sourceAddress}" } },
{ "kind": "invoke", "contract": "pool", "function": "supply",
"args": { "requests": [[{ "tag": "Native" }, "1000"]] } }
]
```

Notes on semantics that shape how you author a sequence:

- **A constructor is just an invoke.** komet runs a function literally named
`__constructor` as an ordinary call, and its storage writes persist — so
express contract initialization as an explicit `invoke` step, not as part of
the deploy.
- **Every transaction gets a distinct hash.** Two byte-identical invokes would
otherwise be deduplicated by the node; the runner assigns an incrementing
account sequence per submission so repeated identical calls each execute.
- **A reverting transaction stays debuggable.** The sequence never stops on a
failed transaction; the traced step's trace is fetched regardless of status.

### `deploy` step

| Field | Required | Description |
|-------|----------|-------------|
| `kind` | ✅ | `"deploy"`. |
| `id` | ✅ | Handle name. Later `invoke` steps reference it via their `contract` field, and `trace` can select this deploy's transaction by this id. Must be unique. |
| `contract` | one of `contract`/`wasm` | Path to a contract crate directory (containing `Cargo.toml`) to build. |
| `wasm` | one of `contract`/`wasm` | Path to a prebuilt `.wasm`. Overrides building from `contract`. |
| `buildCommand` | | Command used to build a `contract` directory. Defaults to `stellar contract build` (or the `soroban.stellar.path` setting). Ignored when `wasm` is given. |
| `debugInfo` | | Build with DWARF debug info for Rust source mapping (default `true`). Set `false` to debug at the wasm level only. Ignored when `wasm` is given. |

### `invoke` step

| Field | Required | Description |
|-------|----------|-------------|
| `kind` | ✅ | `"invoke"`. |
| `contract` | ✅ | Handle `id` of an earlier `deploy` step. |
| `function` | ✅ | Name of the contract function to call. |
| `args` | | Arguments to the function (see [Arguments](#arguments)). |
| `id` | | Optional label so `trace` can select this invoke's transaction. |

## `trace`

`trace` names which transaction feeds the debug session:

| Value | Meaning |
|-------|---------|
| `"last"` (default) | The final step in `transactions`. |
| a number | A 0-based index into `transactions`. |
| a string | A step `id` — a deploy's `id` or an invoke's optional `id`. |

## Arguments

An invoke step's `args` is an **object keyed by the function's parameter
names**. Values follow the contract's own spec, so composite types work
naturally:

| Rust type | JSON value |
|-----------|------------|
| `u32` / `i32` / `bool` | `1`, `-2`, `true` |
| `u64`/`i64`/`u128`/`i128`/`u256`/`i256` | a decimal string, e.g. `"1000"` (avoids JS precision loss) |
| `Symbol` / `String` | `"hello"` |
| `Address` | a `G…` (account) or `C…` (contract) string |
| a unit enum variant | `{ "tag": "Native" }` |
| an enum variant with data | `{ "tag": "Other", "values": [7] }` |
| a tuple or `Vec` | a JSON array, e.g. `[a, b]` |
| a struct | an object keyed by field name |
| `Bytes` | a hex string |

Composites nest, so a `Vec<(Asset, i128)>` where `Asset` is an enum is:

```jsonc
"args": { "requests": [ [ { "tag": "Native" }, "1000" ],
[ { "tag": "Other", "values": ["C…"] }, "-50" ] ] }
```

### Positional arguments

`args` also accepts the lower-level **positional** form: an array of
`{ "type", "value" }` objects, one per parameter, encoded without consulting the
spec. Prefer the named object form above; the positional form is a fallback for
when you want explicit control over each ScVal's type:

```jsonc
"args": [ { "type": "u32", "value": 1 }, { "type": "u32", "value": 2 } ]
```

## Substitution tokens

Two tokens are expanded inside string `args` values (and inside nested
composites):

| Token | Expands to |
|-------|-----------|
| `${sourceAddress}` | The source account's address (the account that signs every transaction). |
| `${contract:<id>}` | The deployed contract address behind the handle `<id>`. Use it to wire one deployed contract's address into another's call. |

Alongside these, the standard VS Code variables such as `${workspaceFolder}`
work in path fields like `contract` and `wasm`.

## Top-level fields

| Field | Description |
|-------|-------------|
| `transactions` | The ordered live sequence (required for live mode). |
| `trace` | Which transaction to debug (see [`trace`](#trace)). |
| `sourceSecret` | Source account secret (`S…`) used to sign every transaction. A deterministic account is derived and self-seeded if omitted; its address is available as `${sourceAddress}`. |
| `node` | Local-network connection/spawn settings: `attach`, `host`, `port`, `command`, `ioDir`. |
| `rawTrace` | Replay mode: path to a recorded JSONL trace to replay instead of running a live sequence. |
| `wasmPath` | Replay mode only: a `.wasm` supplying disassembly and DWARF source mapping for the replayed trace. |

Two VS Code settings let you point at executables that aren't on your `PATH`:
`soroban.stellar.path` and `soroban.kometNode.path`.

## Replay mode

Replay needs no toolchain and no node — it re-reads a trace you (or a teammate)
recorded earlier, which makes it ideal for reproducible bug reports. Give a
`rawTrace` path; add a `wasmPath` to get Rust source mapping instead of the
wasm-level fallback:

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Replay add(4, 3)",
"rawTrace": "${workspaceFolder}/traces/add.trace.jsonl",
"wasmPath": "${workspaceFolder}/../test/fixtures/adder-debug.wasm"
}
```

Record a trace with the [`soroban-trace`](./trace-cli.md) CLI.

## Examples

### 1. Debug a single transaction

The smallest live configuration: build the crate in the workspace folder, invoke
one function, and debug it. There is one deploy and one invoke, and `trace`
defaults to the last step.

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Debug add(1, 2)",
"transactions": [
{ "kind": "deploy", "id": "adder", "contract": "${workspaceFolder}" },
{ "kind": "invoke", "contract": "adder", "function": "add",
"args": { "a": 1, "b": 2 } }
]
}
```

### 2. Deploy a complex system, debug one call into it

Deploy several contracts, initialize them, wire their addresses together with
`${contract:<id>}`, seed some state, then debug a later call. Here we trace the
`swap` invoke by giving it an `id` and pointing `trace` at it — so the earlier
setup runs but the session opens on the call you care about.

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Debug router.swap",
"transactions": [
{ "kind": "deploy", "id": "token_a", "wasm": "${workspaceFolder}/wasm/token.wasm" },
{ "kind": "deploy", "id": "token_b", "wasm": "${workspaceFolder}/wasm/token.wasm" },
{ "kind": "deploy", "id": "router", "contract": "${workspaceFolder}/router" },

{ "kind": "invoke", "contract": "token_a", "function": "__constructor",
"args": { "admin": "${sourceAddress}", "decimals": 7, "name": "A", "symbol": "A" } },
{ "kind": "invoke", "contract": "token_b", "function": "__constructor",
"args": { "admin": "${sourceAddress}", "decimals": 7, "name": "B", "symbol": "B" } },

{ "kind": "invoke", "contract": "router", "function": "__constructor",
"args": { "token_a": "${contract:token_a}", "token_b": "${contract:token_b}" } },

{ "kind": "invoke", "contract": "token_a", "function": "mint",
"args": { "to": "${sourceAddress}", "amount": "1000000" } },

{ "kind": "invoke", "id": "the_swap", "contract": "router", "function": "swap",
"args": { "from": "${sourceAddress}", "amount_in": "1000", "min_out": "990" } }
],
"trace": "the_swap"
}
```

### 3. Complex parameter types

Enums, tuples, structs, nested vectors, addresses, and large integers, all in
named `args`:

```jsonc
{
"type": "soroban",
"request": "launch",
"name": "Debug pool.submit",
"transactions": [
{ "kind": "deploy", "id": "pool", "contract": "${workspaceFolder}" },
{ "kind": "invoke", "contract": "pool", "function": "submit",
"args": {
"from": "${sourceAddress}",
"spender": "${sourceAddress}",
"requests": [
[ { "tag": "Supply" }, "${contract:pool}", "1000000" ],
[ { "tag": "Borrow" }, "${contract:pool}", "-500" ]
],
"config": { "collateral": true, "cap": "170141183460469231731687303715884105727" }
} }
],
"trace": "last"
}
```
Loading