The sdkt-cli crate uses clap (derive API) for command routing. Every command returns Result<(), Box<dyn std::error::Error>>; library errors (RpcError, DecodeError, WasmError) bubble up and are printed via eprintln! with a non-zero exit.
sdkt
├── decode <xdr>
│ ├── --type <ScVal|TransactionEnvelope|TransactionResult|ContractEvent>
│ ├── --format <json|pretty>
│ └── --file <path>
│
├── encode <TYPE:VALUE>...
│ │ (fully offline: encode typed values to base64 XDR)
│ │ Supports: u32, i32, u64, i64, timepoint, duration, u128, i128, bool,
│ │ string, symbol, bytes, address. Also json:<JSON> for composite values.
│
├── inspect <contract-id>
│ ├── --interface (render full interface instead of name-only ABI)
│ ├── --format <json|pretty>
│ ├── --abi <wasm> (ABI-aware storage decode)
│ └── --abi-contract <id> (fetch deployed contract's ABI)
│
├── storage
│ ├── check <contract-id> [--abi <wasm>] [--abi-contract <id>] [--format]
│ ├── analyze <contract-id> [--abi <wasm>] [--abi-contract <id>] [--format]
│ ├── estimate <wasm-path> [--ledgers <N>] [--format <pretty|json>]
│ ├── storage-diff --old <snapshot-a.json> --new <snapshot-b.json> [--extend-plan] [--format <pretty|json>]
│ ├── read --contract <contract-id> --key-xdr <BASE64_XDR> [--abi <wasm>] [--format]
│ ├── extend --contract <contract-id> --ledgers <N> [--key <xdr>]... [--identity <name>] [--timeout <s>] [--interval <s>] [--format]
│ └── restore --contract <contract-id> --envelope <xdr> [--dry-run] [--identity <name>] [--timeout <s>] [--interval <s>] [--format]
│
│ `estimate` performs an offline calculation of storage rent over a specified
│ ledger horizon (default 17,280 ledgers, ~1 day) using the contract spec ABI.
│ It computes a breakdown across Instance, Persistent, and Temporary classes
│ and a total in stroops and XLM.
│
│ `storage-diff` compares two storage snapshots (`--old` and `--new`, generated by
│ `sdkt storage analyze --format json`) and identifies removed, expiring, and unchanged
│ entries. With `--extend-plan`, it derives a non-mutating remediation plan (target contract,
│ affected keys, and suggested ledger horizon) for operator extension.
│
│ Snapshots recorded by `sdkt storage analyze --format json` include capture context:
│ - `latest_ledger`: the ledger sequence at capture time.
│ - `network`: the network identity (passphrase or network name) against which capture was performed.
│
│ When diffing snapshots:
│ - If both snapshots record a network and they differ, the diff is refused with an error
│ naming both networks (preventing cross-network skew from reading as state drift).
│ - If both snapshots record a ledger, TTLs are compared on a common basis by normalising
│ the old snapshot's TTLs by the ledger delta (`new_ledger - old_ledger`), ensuring normal
│ time passage between snapshots does not falsely classify entries as expiring soon.
│ The ledger delta is surfaced in both pretty and JSON output.
│ - Backward compatibility: legacy snapshot files omitting capture context remain fully
│ supported; their capture context is reported as "unknown".
│ - Value drift detection: `sdkt storage analyze --format json` records each entry's
│ base64 XDR `ScVal` value. When two snapshots both carry values for the same entry,
│ `storage-diff` reports a `value_changed` delta (before/after values) instead of
│ classifying the entry as unchanged. An entry whose value and TTL both match stays
│ unchanged; TTL-only changes still report only TTL/rent deltas. Entries without
│ captured values (legacy snapshots, contract-code entries) are never flagged for
│ value drift.
│
│ `read` fetches a single ledger entry by its complete `LedgerKey` (base64 XDR).
│ The instance key is NOT included automatically — supply the full key via --key-xdr.
│ ABI formatting is optional and applies only to the returned value.
│
│ `extend` submits an `ExtendFootprintTtl` transaction. The contract instance
│ key is always included in the read-only footprint. Additional `--key` values
│ (base64 XDR or hex XDR) are merged and de-duplicated. This does NOT discover
│ all contract storage and does NOT restore archived entries.
│
│ `analyze` (alias `snapshot`) accepts repeatable `--key-xdr` (alias `--key`). Each
│ `getLedgerEntries` request is limited to 200 keys; larger read sets (including
│ the automatically included instance key) are fetched in sequential batches.
│ For batched reads, found entries retain request order and absent entries are
│ omitted. A failed batch or a change of ledger between batches fails the
│ complete read; retry if the ledger changes. This batching applies to reads,
│ not to the transaction footprint limits of `extend` or `restore`.
│
│ `restore` simulates `--envelope` (the invocation that failed on archived
│ state), adopts the `restorePreamble` footprint and minimum resource fee, and
│ submits a `RestoreFootprint` transaction. It fails without submitting when
│ the simulation has no preamble (state is live) or the preamble footprint is
│ empty or malformed. `--dry-run` prints the keys and fee without signing or
│ submitting. It restores only; re-run the invocation afterwards.
│
│ `--abi <wasm>` supplies the ABI from a local WASM; `--abi-contract <id>` fetches
│ the deployed contract's on-chain WASM and uses it as the ABI source
│ for storage decoding. The two flags are mutually exclusive.
│
├── invoke <CONTRACT_ID> <FUNCTION>
│ ├── --args <TYPE:VALUE>... (same typed-args as `call` / `tx build`;
│ │ strict: unknown types are rejected)
│ ├── --identity <name> (signs and pays; default: "default")
│ ├── --timeout <SECONDS> (wait-for-settlement timeout; default 60)
│ ├── --interval <SECONDS> (poll interval while waiting; default 1)
│ ├── --format <json|pretty>
│ └── --network-profile <NAME> / --rpc-url <URL> / --network-passphrase <P>
│
│ Polling rules (shared by every command that waits for a transaction):
│ `--interval` must be greater than 0 and `--timeout` must be greater than
│ or equal to `--interval`; invalid pairs are rejected before any network
│ or identity work begins. `--no-wait` returns the transaction hash right
│ after submission and ignores both flags.
│
│ State-changing end-to-end flow in one command:
│ fetch account sequence → simulate → build final envelope (authoritative
│ footprint + fees + auth entries from simulation) → sign with the local
main
│ Result decoding is limited to the transaction-level `TransactionResult`
│ XDR (auto-detection also recognizes `ScVal`, `TransactionEnvelope`,
│ `TransactionResult`, and `ContractEvent`; there is no XDR
│ `TransactionStatus` type in the pinned `stellar-xdr` 28.0.0 API).
│ Inherits the mainnet safety guard
│ (see below). Live Testnet smoke test is documented but NOT exercised in CI.
│
├── tx
│ ├── inspect <hash> [--format]
│ ├── validate <xdr> [--format] (offline parse + structural checks)
│ ├── decode <xdr> [--format] (offline decode; human-readable transaction envelope)
│ ├── simulate <xdr> [--format] [--abi <wasm>] [--abi-contract <id>] (RPC; surfaces restore preambles, costs, state changes; ABI-aware result decoding. `--abi-contract` fetches the deployed contract's on-chain WASM for decoding)
│ ├── sign [--input <xdr|file>] [--output <file>] [--identity <name>] [--network <testnet|mainnet|futurenet|custom:<p>>] [--format] (offline ED25519 signing)
│ ├── submit <xdr> [--wait] [--timeout <s>] [--interval <s>] [--format] (RPC)
│ └── build [--source --sequence --contract --function --fee* --arg* --output --memo-text <text> --memo-id <id>]
│ Offline by default: the envelope carries only the
│ base inclusion fee (100 stroops) and prints a
│ warning, because a Soroban submission also needs
│ the resource fee.
│ Pass the network flags on `tx` (e.g.
│ `sdkt tx --network-profile testnet build ...`) to
│ simulate the invocation and adopt the reported
│ `minResourceFee` and footprint, exactly as
│ `invoke` does — the resulting envelope submits
│ without re-simulating.
│ `--fee <STROOPS>` overrides both and costs no
│ network round trip.
│ `--memo-text <TEXT>` (at most 28 bytes) or
│ `--memo-id <ID>` attaches a Stellar memo to the
│ built envelope; the two flags are mutually
│ exclusive. Omit both for no memo (unchanged
│ default).
│
├── events <contract-id>
│ ├── --format <json|pretty>
│ ├── --start-ledger <START_LEDGER> (start ledger sequence number for event search)
│ ├── --end-ledger <END_LEDGER> (end ledger sequence number for event search)
│ ├── --topic <SYMBOL> (filter by first topic/event name; server-side filtering)
│ ├── --abi <wasm> (ABI-aware decode from a local WASM)
│ ├── --abi-contract <id> (ABI-aware decode using deployed contract's on-chain WASM)
│ ├── --network-profile <NAME> / --rpc-url <URL> / --network-passphrase <P>
│ └── [other network resolution flags]
│
├── account <address> [--format]
│
├── fee
│ └── estimate (manual value entry, type-prefixed)
│
├── wasm
│ ├── inspect <file.wasm> Offline inspection of a local WASM file (sections, exports, spec)
│ ├── metadata --contract <contract> [--network testnet] [--refresh] [--format]
│ └── cache (info | remove | clear)
│
├── verify --contract <contract-id>
│ ├── --wasm <file.wasm> (local artifact to compare; offline hashed)
│ ├── --network <testnet> (RPC network)
│ ├── --format <json|pretty>
│ └── --upgrade-safety (compare the live deployed contract's interface
│ against --wasm candidate; emits UpgradeVerdict.
│ Requires --wasm. Read-only; inherits mainnet-safety
│ guard.)
│
├── deployment-verify --contract <contract-id>
│ ├── --wasm <file.wasm> (local artifact to compare; offline hashed)
│ ├── --network <testnet> (RPC network; conflicts with --rpc-url)
│ └── --format <json|pretty>
│
│ Read-only. Verdicts: MATCH (exit 0), DRIFT (exit 1), UNKNOWN (exit 1,
│ reason given), NOT_FOUND (exit 1). Probes the raw contract-instance
│ ledger entry — no inspection shortcuts, no bytecode download. A
│ `stellar_asset` executable has no WASM artifact, so it is UNKNOWN, not
│ a match. Never signs, submits, or deploys.
│
├── health --contract <contract-id>
│ ├── --wasm <file.wasm> (optional local artifact to verify against)
│ ├── --network <testnet> (RPC network / report label)
│ └── --format <json|pretty>
│
├── diff
│ ├── --old-wasm <A>
│ ├── --new-wasm <B>
│ ├── --format <json|pretty>
│ ├── --upgrade-safety (emit UpgradeVerdict; exclusive with size policy)
│ ├── --max-growth-pct <N> (fail when growth over the old artifact exceeds N%)
│ └── --max-size-bytes <N> (fail when the new artifact exceeds N bytes)
│
├── audit [path.rs]
│ ├── --list-rules (list available audit rules and exit)
│ ├── --format <json|pretty>
│ ├── --disable <RULE_ID> (repeatable)
│ ├── --rules <PATH|ID> (repeatable; compiled plugin artifact or installed plugin ID)
│ ├── --no-plugins (skip loading installed plugins)
│ ├── --abi <wasm> (path to contract WASM for spec-correlated analysis)
│ ├── --abi-contract <id> (contract ID whose deployed WASM provides ABI)
│ ├── --fail-on <SEVERITY> (exit non-zero if findings at or above this level; critical|warning|info)
│ └── --network-profile <NAME> / --rpc-url <URL> / --network-passphrase <PASS>
├── release-assurance
│ ├── --wasm <WASM> (candidate artifact; required)
│ ├── --previous-wasm <WASM> (baseline for the offline upgrade diff)
│ ├── --audit <PATH>... (Rust source path(s)/dir(s); same engine as `audit`)
│ ├── --disable <RULE_ID> (repeatable)
│ ├── --contract <ID> (deployed contract for on-chain checks; skipped if omitted)
│ ├── --network <testnet|mainnet|futurenet> (network for the on-chain checks; with --contract)
│ ├── --fuzz-evidence <EVIDENCE_JSON> (attach JSON evidence from sdkt fuzz campaign)
│ ├── --max-size-bytes <N> (fail when the candidate exceeds N bytes)
│ ├── --max-growth-pct <N> (fail when growth over --previous-wasm exceeds N%)
│ ├── --format <json|pretty>
│ ├── --network-profile <NAME> / --rpc-url <URL> / --network-passphrase <PASS>
│ └── [other flags inherited from network-profile resolution]
├── identity
│ ├── generate <name> [--format pretty|json]
│ ├── import <name> <secret> (required)
│ │ stdin form (recommended): import <name> -
│ │ ⚠ argv secrets are deprecated (issue #64): pass `-` instead
│ ├── list [--format pretty|json]
│ ├── show <name> [--format pretty|json]
│ ├── delete <name>
│ ├── default <name>
│ └── fund <name> [--network-profile <NAME>] [--format pretty|json]
│
├── init <name> [--minimal] [--force] [--format]
├── network
│ ├── add <name> [--rpc-url <URL>] [--passphrase <PASS>] [--friendbot <URL>] [--description <TEXT>]
│ ├── list
│ ├── show <name> [--format json|pretty]
│ ├── check <name> [--format json|pretty]
│ ├── diagnose [--network <testnet>] [--rpc-url <URL>] [--format json|pretty]
│ └── remove <name>
│
│ `diagnose` is read-only: getHealth/getNetwork/getLatestLedger plus two
│ Horizon GETs. Reports passphrase identity (match/mismatch/unknown),
│ protocol agreement across RPC and Horizon, the local sdkt version, and
│ the ledger resource limits Horizon actually observed
│ (base_fee_in_stroops, base_reserve_in_stroops, max_tx_set_size,
│ protocol_version) — `unavailable` with a reason when Horizon does not
│ answer. Limits are never hardcoded. Exit 0 only when status is `ok`;
│ identity mismatch, protocol inconsistency or an unreachable endpoint exit
│ 1 with a valid report. Never signs, submits, or deploys.
├── build Compile Rust contracts in the workspace into WASM artifacts
│ └── --format <json|pretty> (default: pretty)
├── lock Generate or inspect `sdkt.lock`
│ ├── generate Write `sdkt.lock` from current build artifacts (run `sdkt build` first)
│ ├── verify Verify `sdkt.lock` against on-disk contract artifacts AND package
│ │ dependencies (lock matches manifest, git commits, path existence).
│ │ Advisory and non-fatal: prints `✓ lock file verified` /
│ │ `✓ package dependencies verified` or lists every drift.
│ └── show Print `sdkt.lock` contents
├── package Validate local package manifests
│ ├── validate Offline-validate `[package]` metadata + local `[dependencies]`
│ │ path graph (no network/registry; git/* sources rejected)
│ ├── fetch Fetch deps into `.sdkt-cache`: local path passthrough,
│ │ git clone/checkout. `--force` updates. Never builds.
│ ├── update Synchronize deps: refresh git deps to latest available
│ │ commit and rewrite `sdkt.lock`. `rev` pinned; `tag`/`branch`
│ │ update on drift. `--check` reports; `--dry-run` previews.
│ ├── pack Bundle the resolved project into a portable offline artifact
│ │ manifest + lock + cached git checkouts. `--out`, `--format`.
│ └── publish Validate publish readiness (`--dry-run` only, read-only);
│ detects missing cache, lock drift, integrity mismatch.
### Synchronizing dependencies
`sdkt package update` closes the package loop: `validate → fetch → update → verify`.
It resolves each git dependency's **currently available** commit via `git ls-remote`
against its declared URL (no checkout, no clone in `--check`/`--dry-run`), then:
- **`rev`** — immutable and already pinned; never updated. Reported as `pinned (rev)`.
- **`tag`** — resolves the tag's current commit; if it differs from the lock, the
cache is refreshed and the lock is rewritten.
- **`branch`** — fetches the latest branch head, updates the cache, and rewrites the
lock.
- **local `path`** — no remote; reported `unchanged`.
Flags:
- `--check` — report available updates only; **does not** fetch or rewrite the lock;
exits 0 (non-zero is reserved for hard errors like a missing lock or invalid manifest).
- `--dry-run` — compute everything and preview what would change; the cache and lock
are left untouched.
- `--format pretty|json` — `json` emits `{"checked","updated","unchanged","changes":[...]}`.
On success the lock records the new `commit_sha`, `resolved_reference`, `cache_location`,
and `integrity` for git deps — contract entries, artifact hashes, and deploy order are
preserved. Clear errors are produced for: missing cache, git unavailable, invalid
manifest, missing lock, detached branch, unknown reference, and network failure.
### Version-constrained dependencies
A git dependency may declare an optional semver `version` constraint instead of a
fixed `tag` / `branch` / `rev`:
```toml
[dependencies.math]
git = "https://github.com/org/math"
version = ">=1.0, <2"
When version is set without an explicit ref, sdkt package fetch / sdkt package update resolve the highest remote tag that satisfies the constraint (via
git ls-remote --tags; offline for local remotes) and materialize / lock that exact
tag. --check reports constraint unsatisfied when no tag matches. An explicit
tag / branch / rev always takes precedence and the constraint is ignored
(mirroring how rev / path deps bypass version resolution). The lock records the
resolved version for audit. This reuses the existing fetch / cache / lock
infrastructure; the only new logic is a single pure VersionResolver.
sdkt package pack bundles the fully resolved project into a portable,
network-free artifact so it can be reconstructed and rebuilt on another machine
without contacting any remote:
- the manifest (
.sdkt.toml), - the lockfile (
sdkt.lock), - the cached git dependency checkouts under
.sdkt-cache/git/<cache_key>.
Each artifact carries a package.json descriptor (PackageBundle) recording the
package name/version, the chosen format, the sdkt.lock sha256, and a
per-dependency entry (source, git_url, commit_sha, integrity,
cache_key, version) so the bundle can be verified offline.
Flags:
--out <DIR>— output directory (default./dist).--format tar.zst|dir—tar.zstwrites a compressed tarball<out>/<name>-<version>.tar.zst;dirwrites a directory tree<out>/<name>-<version>/. Any other value is rejected with a clear error.
Pack preconditions: every git dependency declared in the manifest must
have a cached checkout under .sdkt-cache/git/<cache_key>. If a declared
dependency's checkout is missing, sdkt package pack exits non-zero with a
git dependency '<name>' is declared but its checkout is not cached ... error
and writes no descriptor — the bundle never claims a dependency it does not
contain.
sdkt package publish --dry-run validates publish readiness using the
existing manifest/lock/cache/integrity infrastructure. It detects missing cache,
lock drift, integrity mismatch, commit mismatch, reference change, and invalid
package state — all read-only, no network, nothing is published. --broadcast is
explicitly opt-in and is rejected because no registry source is defined; the
workflow remains fully offline.
Round-trip: a bundle can be reconstructed (sdkt_core::package::unpack) and the
reconstructed tree verified to reproduce the original sdkt.lock sha256 and
per-git-dependency integrity exactly (verify_bundle_equivalence) — no hashing
or git logic is duplicated; the same compute_dependency_integrity /
git_cache_key primitives are reused.
├── project
│ └── deploy Deploy all contracts defined in the workspace (.sdkt.toml),
│ applying topological dependency sorting
│
│ --timeout <SECONDS> (default 60) and --interval <SECONDS> (default 1)
│ control how long each contract deployment waits for settlement and how
│ often it polls; both are validated before any network or identity work.
│
│ Contracts declare dependencies via depends_on (canonical) or the
│ legacy deploy_after field in [contracts.<alias>]; both are merged. Build,
│ deploy, and sdkt lock generate share one resolver, so order is deterministic.
│ Invalid graphs (unknown/self/duplicate dependency, cycle, duplicate name)
│ fail fast with a clear error.
│
│ Deployment records: every successfully deployed contract is written to
│ .sdkt-deployments.json in the project directory, keyed by network scope
│ and then alias:
│
│ {
│ "profiles": {
│ "testnet": {
│ "token": {
│ "contract_id": "C…",
│ "wasm_hash": "60cddae6…",
│ "network": "testnet",
│ "timestamp": 1700000000,
│ "salt": "deploy"
│ }
│ }
│ }
│ }
│
│ The scope key is the explicit --network-profile <NAME> when given, else the
│ network derived from the passphrase (testnet/mainnet/futurenet) or a
│ custom-<hash> for unknown passphrases — so testnet and mainnet deploys never
│ collide. Records are persisted after every contract, so if a deploy fails
│ mid-graph the contracts that already landed are never lost; the alias →
│ contract ID mapping is retained for the retry. On success the record is written
│ too (additive; the stdout JSON/pretty output is unchanged).
│
│ --skip-deployed resumes an interrupted deploy: aliases whose recorded
│ contract ID is verified to still exist on-chain (via getLedgerEntries) are
│ skipped without re-deploying (and re-paying for) them, printed as
│ ✓ '<alias>' already deployed at <contract_id>. A record whose contract no
│ longer exists on-chain is NOT trusted — sdkt warns
│ ⚠ Recorded contract for '<alias>' is no longer on-chain; re-deploying. and
│ deploys it fresh, replacing the stale record. The default identity (see
│ sdkt identity) is used to sign all deployments.
│
│ Constructor arguments: contracts requiring initialization parameters at
│ create time declare them in .sdkt.toml via the ctor_args field:
│
│ [contracts.token]
│ path = "contracts/token"
│ ctor_args = ["address:G...", "u32:1000000"]
│
│ Each string is a TYPE:VALUE pair (same format as sdkt deploy --arg: u32:100,
│ address:G..., string:hello, bool:true, bytes:deadbeef, etc.) or a
│ base64-encoded ScVal. Invalid arguments fail before WASM upload (fail-fast
│ validation) with the same error messages as the single-contract deploy path.
│ Contracts without ctor_args deploy with empty constructor arguments (no
│ change from pre-feature behavior).
│
├── call
│ ├── <CONTRACT_ID>
│ ├──
│ ├── --args TYPE:VALUE... (e.g. u32:100, address:G..., string:hello)
│ ├── --abi (ABI-aware result decoding from a local WASM;
│ │ without it the raw base64 XDR result is shown)
│ ├── --abi-contract (ABI-aware result decoding using the deployed
│ │ contract's on-chain WASM, fetched via the
│ │ inspection path; no local artifact needed.
│ │ Mutually exclusive with --abi.)
│ ├── --format <json|pretty>
│ └── --network-profile
└── deploy
├── --wasm (upload WASM, then create the contract)
├── --wasm-hash (create-only from an already-uploaded 64-char
│ hex WASM hash; mutually exclusive with --wasm)
├── --salt (optional; 40 hex chars, auto-generated if omitted)
├── --show-address (print the predicted address before submission;
│ requires --wasm)
├── --dry-run (predict without submitting; requires --wasm)
├── --format <json|pretty>
├── --deny-breaking (abort if not backwards-compatible; requires --wasm)
├── --old-wasm (baseline, required by --deny-breaking)
├── --identity (signing identity; defaults to default)
├── --timeout (wait-for-settlement timeout for the upload and
│ create transactions; default 60)
├── --interval (poll interval while waiting; default 1)
├── --arg TYPE:VALUE... (constructor arguments)
└── --network-profile / --rpc-url / --network-passphrase
--wasm-hash skips the upload transaction and submits only contract creation,
which is useful to resume after an upload succeeded but creation failed.
Prediction-only options (--dry-run, --show-address) and
--deny-breaking are rejected with --wasm-hash, because these operations
require the local WASM bytes.
├── plugin Manage local audit plugins (install/remove/list/show/update)
│ ├── init [--force] [--format] Scaffold a new audit rule crate
│ ├── list [--format] List installed plugins
│ ├── show [--format] Show metadata for an installed plugin
│ ├── install
## Network Profiles
### Checking an RPC profile
`sdkt network check <name> [--format json]` performs read-only RPC checks against
the saved profile. It checks ledger reachability and node health, then asks the
endpoint for its network identity with `getNetwork`. The output shows the
profile's configured passphrase beside the endpoint-reported passphrase and
warns when they differ; an available Friendbot URL is shown as well. A mismatch
is advisory and does not change the profile or the command's exit status.
Some RPC providers do not implement `getNetwork`. In that case the command
still reports reachability, health, latest ledger, and protocol version from
the other RPC checks, and labels the identity fields as not reported. JSON
output keeps the existing `profile`, `rpc_url`, `reachable`, `status`,
`latest_ledger`, `protocol_version`, and `error` keys, and adds
`configured_passphrase`, nullable `endpoint_passphrase`, nullable
`friendbot_url`, and nullable `network_info_error`. Only an unreachable or
unhealthy endpoint causes a non-zero exit; failure of the optional identity
query does not.
Every RPC command (`inspect`, `verify`, `health`, `storage`, `events`, `account`,
`tx`, `fee`, `wasm`, `deploy`, `project deploy`) accepts the same three optional
flags for selecting / overriding the network endpoint:
| Flag | Meaning |
|------|---------|
| `--network-profile <NAME>` | Use a saved profile (see `sdkt network`). Loads its RPC URL + passphrase. |
| `--rpc-url <URL>` | Explicit RPC endpoint; overrides the profile and `.sdkt.toml`. |
| `--network-passphrase <PASSPHRASE>` | Explicit network passphrase; overrides the profile and `.sdkt.toml`. |
**Resolution precedence (highest wins):**
explicit --rpc-url / --network-passphrase > --network-profile > .sdkt.toml [network] > NetworkConfig::default() (testnet)
`tx sign` is excluded — it is offline signing and takes only `--network` for the
signature hash. Commands invoked without these flags behave exactly as before.
## Shell completions
`sdkt` can emit shell completion scripts for the major shells. This is the
fastest way to discover subcommands and flags.
```bash
sdkt completions bash # bash
sdkt completions zsh # zsh
sdkt completions fish # fish
sdkt completions powershell # powershell
sdkt completions elvish # elvish
Pipe the output to a file your shell reads at startup (see the README "Shell completions" section for per-shell install paths). Tab-completion then covers commands, subcommands, and flag names.
sdkt doctor runs baseline diagnostic checks on the sdkt environment and the
current project. It is fully offline and safe to run anywhere.
sdkt doctor # human-readable output
sdkt doctor --json # machine-readable outputChecks included in the core:
| Check | ID | Meaning |
|---|---|---|
| sdkt runtime | sdkt-runtime |
confirms the binary is running and reports its version |
| Rust toolchain | rust-toolchain |
verifies cargo and rustc are on PATH |
| WASM build target | wasm-target |
verifies a Soroban WASM target is installed via rustup |
| Project config | project-config |
validates .sdkt.toml and the [contracts] dependency graph when inside a project |
Exit codes:
0— all checks passed, or only warnings (non-fatal)1— at least one check failed (e.g. invalid config, missing WASM target)
Warnings are never fatal. The JSON output contains no secret material.
sdkt encode converts a typed value to its base64 XDR representation — the
write-direction counterpart to sdkt decode. It is fully offline and useful
for building test fixtures, debugging, and CLI round-trips.
sdkt encode u32:100
# AAAAAwAAAGQ=
# Verify by decoding back
sdkt encode string:hello | xargs sdkt decode --type ScVal
# {"string": "hello"}
sdkt encode symbol:USD | xargs sdkt decode --type ScVal
# {"symbol": "USD"}
sdkt encode u128:340282366920938463463374607431768211455 | xargs sdkt decode --type ScVal
# {"u128": "340282366920938463463374607431768211455"}
sdkt encode i128:-1000 | xargs sdkt decode --type ScVal
# {"i128": "-1000"}
sdkt encode timepoint:1758000000 | xargs sdkt decode --type ScVal
# {"timepoint": "1758000000"}
sdkt encode duration:86400 | xargs sdkt decode --type ScVal
# {"duration": "86400"}
sdkt encode bytes:000aFF | xargs sdkt decode --type ScVal
# {"bytes": "000aff"}
sdkt encode json:'[1,2,3]' | xargs sdkt decode --type ScVal
# {"vec": [{"u32": 1}, {"u32": 2}, {"u32": 3}]}u32, i32, u64, i64, timepoint, duration, u128, i128, bool, string,
symbol (up to 32 bytes), bytes, address (Stellar G... strkey).
Exactly one value is encoded per invocation; the TYPE:VALUE syntax matches
the typed-argument convention used by sdkt call and sdkt invoke.
u128 and i128 accept decimal integers within their full 128-bit ranges.
Their decoded JSON values are decimal strings, preserving all digits.
bytes accepts hexadecimal pairs without a 0x prefix, with either hex
letter case. Surrounding whitespace is trimmed; empty input (bytes:)
encodes empty bytes. Leading zero bytes are preserved, and decoded JSON
uses lowercase hex. Pair parsing matches the runtime typed-argument parser,
including its acceptance of a leading + in a pair (bytes:+f encodes 0f).
json:<JSON> encodes one JSON value, including nested arrays and objects, as a
single ScVal: json:[1,2,3] is one Vec, not three values. The mapping is:
array to Vec, object to Map with String keys (sorted), null to Void,
true/false to Bool, string to String, and an integer to the smallest
fitting u32/u64 (i32/i64 when negative). Floats and integers beyond
the 64-bit range are rejected. Invalid JSON fails with
invalid JSON in 'json:<input>': <reason>.
Other types, such as vec:, map:, Option, Result, and UDTs, are
rejected with an error listing the supported types (use json: for Vec
and Map). Malformed values (bad or out-of-range
numbers, invalid bools, invalid strkeys, odd-length or invalid hex) fail
with a message naming the offending value.
sdkt generate client produces deterministic, typed Rust call builders from
the ContractSpec of a compiled Soroban contract WASM. The command is fully
offline — it reads a local .wasm artifact and emits Rust source.
# Print the generated client to stdout
sdkt generate client target/wasm32v1-none/release/my_contract.wasm
# Write it to a file
sdkt generate client contract.wasm --output src/client.rs
# Generate a partial client, skipping unsupported functions
sdkt generate client contract.wasm --skip-unsupported --output src/client.rsEach contract function becomes a *Call struct with:
- one typed field per parameter,
- a
NAMEconstant with the contract function name, - an
args()method encoding the call asTYPE:VALUEstrings — the same typed-argument convention accepted bysdkt callandsdkt invoke, - a
<Function>Outputtype alias mirroring the return type.
The output is deterministic: the same input WASM always produces byte-identical source, suitable for checking generated files into a repo.
Parameters and single return values of these primitive types are supported:
u32, i32, u64, i64, bool, address, string, symbol, void
(no return).
By default, any other type — UDTs, Option, Result, Vec, Map, Tuple,
BytesN, Val, multiple return values — aborts generation with an error naming
the function, the type, and the position (parameter or return). Nothing partial
is emitted. A WASM without a contractspecv0 section is rejected as
"not a Soroban contract".
Pass --skip-unsupported to generate call builders for the supported subset of
functions instead of aborting the entire command:
- Supported functions are generated in original spec order.
- Skipped functions and their specific reasons are listed deterministically in the generated file header comment.
- If all functions in the contract are supported, output is identical to the default (no-flag) output.
- If all functions in the contract are unsupported, valid non-crashing output is
produced with an empty
contract_functions()and an explanatory header.
sdkt fuzz provides Soroban-aware deterministic fuzzing for release assurance. It is fully offline and read-only: contracts execute in an isolated in-memory host environment without touching the network.
sdkt fuzz campaign <wasm> runs a deterministic fuzz campaign against a compiled contract.
Syntax:
sdkt fuzz campaign contract.wasm \
--seed 42 \
--cases 100 \
--function increment \
--auth correct-auth \
--artifact-dir ./sdkt-fuzz-artifacts \
--expect-success increment \
--expect-error invalid:Auth:6 \
--replay \
--format jsonArguments and flags:
| Flag | Default | Meaning |
|---|---|---|
<WASM> |
required | Path to the compiled contract WASM file |
--seed <N> |
0 |
Deterministic seed (u64). Same seed produces identical test case sequence across runs |
--cases <N> |
100 |
Number of test cases to execute |
--function <NAME> |
all | Restrict the campaign to one exported contract function |
--artifact-dir <DIR> |
./sdkt-fuzz-artifacts |
Directory where finding artifacts are written |
--auth <MODE> |
no-auth |
Authorization mode per test case: no-auth, correct-auth, or wrong-auth. See the sdkt-fuzz docs for exact semantics |
--expect-success <FN> |
none | Declare that function <FN> must succeed (repeatable). Violations produce RETURN_MISMATCH or UNEXPECTED_ERROR findings |
--expect-error <FN:TYPE:CODE> |
none | Declare that function <FN> must fail with error type <TYPE> and code <CODE>, e.g. --expect-error bump:Auth:6 (repeatable) |
--replay |
false | Replay every written artifact to verify determinism and reproduce findings |
--format <FORMAT> |
pretty |
Output format: pretty, json, or jsonl |
Expectation declarations:
--expect-success <FN>— declare that a function must always return successfully. If a case returns an error or value mismatch, it produces a finding.--expect-error <FN:TYPE:CODE>— declare that a function must fail with a specific error. Format:FunctionName:ErrorType:ErrorCode. Example:--expect-error transfer:Auth:6means thetransferfunction must fail withContractError{Auth, 6}.- A function may have only one expectation; declaring it with both flags is a usage error (exit 2).
- Malformed declarations are usage errors (exit 2).
Output artifacts:
Finding artifacts are written to --artifact-dir as JSON files following the naming pattern <contract>-<function>-case<NNNN>.json. Each artifact contains:
artifact_hash— SHA-256 of the canonical artifact JSON (used for deduplication and replay verification)wasm_hash— SHA-256 of the WASM that produced the findingcampaign_seed— seed used to generate the test case sequencecase_id— zero-based case index in the campaigninput— the typed arguments, seed, and auth mode for this caseobserved_result— the actual return value or error from contract executionclassification— the finding type:Pass,ExpectedError,ReturnMismatch,UnexpectedError, etc.minimized— whether the case was minimizedcomplexity— case complexity before and after minimizationobservation— structured error/return data for replayauth_per_step— authorization entries for each step in a multi-step sequence (if applicable)
Supported --format values:
| Format | Purpose |
|---|---|
pretty |
Human-readable report; findings listed with classification and details |
json |
Single JSON document: campaign metadata + findings array |
jsonl |
JSON Lines: one artifact per line for streaming processing |
Exit codes:
0— campaign executed successfully; findings may or may not have been discovered1— runtime error (e.g. WASM load failure, host error, invalid arguments)2— usage error (e.g. malformed expectation, invalid seed)
sdkt fuzz replay <artifact> replays a finding artifact against its WASM to verify it reproduces.
Syntax:
sdkt fuzz replay ./sdkt-fuzz-artifacts/contract-increment-case0042.json \
--wasm contract.wasmArguments and flags:
| Flag | Meaning |
|---|---|
<ARTIFACT> |
Path to the finding artifact (JSON) produced by a campaign |
--wasm <WASM> |
required — Path to the campaign WASM the artifact was produced from |
Replay verdict:
| Exit code | Verdict | Meaning |
|---|---|---|
0 |
REPRODUCED |
The artifact reproduced: same input, same result, finding verified |
1 |
MISMATCH |
The artifact did not reproduce: result differs from expected |
2 |
Usage error | Invalid artifact or WASM path |
Determinism:
If --replay was used during the campaign, every artifact was automatically replayed to verify determinism. A REPRODUCED verdict confirms that the finding can be independently re-executed with the exact same inputs and produce the same result.
By default, artifacts are written to ./sdkt-fuzz-artifacts with this structure:
./sdkt-fuzz-artifacts/
├── <contract-hash>-<function-name>-case0000.json
├── <contract-hash>-<function-name>-case0001.json
├── <contract-hash>-<function-name>-case0042.json
└── ...
Each JSON file is a complete finding artifact, self-contained and ready for replay.
For details on fuzz evidence from real Soroban contracts and multi-step execution, see Real-Contract Fuzz Evidence.
sdkt release-assurance accepts --fuzz-evidence <EVIDENCE_JSON> to attach fuzz evidence to a release report. The release pipeline does not run fuzz campaigns itself; campaigns are run separately (manually or in CI) and the resulting evidence is attached by path.
A local, offline-first plugin store. All operations are local;
there is no hosted registry and no remote source. Plugins are referenced by a
stable id declared in their plugin.toml.
sdkt plugin list # list installed plugins
sdkt plugin list --format json # JSON output; every plugin subcommand accepts --format json
sdkt plugin init ./path/to/my-rule # scaffold a new audit rule project
sdkt plugin show <id> # show a plugin's metadata
sdkt plugin install ./path/to/artifact.wasm # install from a local file
sdkt plugin install ./bundle.sdktplugin # verify, then install a bundle (reports signed: true/false)
sdkt plugin install ./bundle.sdktplugin --public-key ./pubkey.key # require a signature from this author key (unsigned is refused)
sdkt plugin remove <id> # remove (idempotent)
sdkt plugin update <id> ./path/to/artifact.wasm # local-only update
sdkt plugin pack ./path/to/plugin-dir --output ./myrule.sdktplugin # pack into .sdktplugin bundle
sdkt plugin pack ./path/to/plugin-dir --secret-key ./secret.key # sign bundle with Ed25519 secret key (32 bytes, raw)
sdkt plugin verify-bundle ./bundle.sdktplugin # verify unsigned bundle integrity
sdkt plugin verify-bundle ./bundle.sdktplugin --public-key ./pubkey.key # verify signed bundle against a specific public key
sdkt audit contract.rs --rules <id> # resolve id → stored artifactsdkt audit --rules <id> resolves an installed plugin ID to its stored
artifact; filesystem paths must point to compiled .so, .dylib, .dll, or
.wasm plugin artifacts. Passing a directory prints a warning and the audit
continues with the built-in rules.
Installing a native plugin prints a warning: native plugins run unsandboxed
(unmodified behavior). See docs/plugins/plugin-authoring.md for the plugin.toml
schema and the install-validation rules.
Store root precedence (lowest → highest): <cwd>/.sdkt/plugins,
<config-dir>/sdkt/plugins, $SDKT_PLUGIN_DIR.
-
--format jsonis supported on all read-style commands, everypluginsubcommand, and ondiff,audit,deploy,initfor scripting / CI. -
diff --upgrade-safetyanddeploy --deny-breakingimplement the Upgrade Safety Guard (seeROADMAP.md). -
WASM size policy (opt-in).
diffandrelease-assuranceaccept--max-size-bytes <N>(absolute ceiling on the candidate artifact) and--max-growth-pct <N>(growth over the baseline). Thresholds are operator-supplied policy — SDKT hardcodes no Stellar network limit, and passing a threshold does not assert network-limit compliance.diffmeasures growth against--old-wasm;release-assurancerequires--previous-wasmfor a growth check and fails with an explicit error when it is missing rather than silently skipping. Violations exit non-zero:diffprints the violation on stderr after the report, andrelease-assurancemarks the artifact sectionFAIL(sorelease_statusbecomesFAIL). Boundaries are inclusive — a size exactly at the limit or growth exactly at the cap passes.diff --format jsonalso reportssize_delta_bytes(signed) andsize_delta_pct(one decimal,nullwhen the old artifact is zero bytes). Size policy is independent of ABI compatibility: it is refused in combination withdiff --upgrade-safety, and a size violation never changes an upgrade-safety verdict.# Report size change alongside the ABI diff (always on, no flags needed): sdkt diff --old-wasm prev.wasm --new-wasm candidate.wasm --format json # Fail the build on a >10% growth OR a >128 KiB candidate (your numbers): sdkt diff --old-wasm prev.wasm --new-wasm candidate.wasm \ --max-growth-pct 10 --max-size-bytes 131072 # Gate a release on the same policy, inside the full assurance report: sdkt release-assurance --wasm candidate.wasm --previous-wasm prev.wasm \ --max-growth-pct 10 --max-size-bytes 131072
-
auditimplements the static-analysis rules (AUTH-001/002/003/004, MOVE-001, MATH-001, CEI-001). -
AUTH-001/AUTH-004 severity follows observed behavior, not the name. Both rules start from a privileged-name heuristic (
initialize/init,set_*/grant_*/revoke_*prefixes, and the substringsadmin,mint,burn,pause,unpause,upgrade,withdraw,configure,freeze,ban,drain,set_admin,owner_set,transfer_ownership,change_owner,set_auth), then decide severity from what the function actually does:- a method of a
#[contractimpl]block that performs a recognized state write or an external call and never callsrequire_auth()→ critical (the unguarded-privilege case); - a function that is not a
#[contractimpl]method (a plainfnor a method of an un-annotated impl) → info: external callers cannot reach it, so authorization is the responsibility of the entrypoint that calls it; - a
#[contractimpl]method that returns a value with no recognized state write or external call → info: positive evidence of a read-only accessor (admin_balance,get_paused), which cannot be a missing-authorization defect; - a
#[contractimpl]method that returns nothing with no recognized side effect → critical: the write may go through a helper the scanner does not recognize, so the finding stays loud.
Nothing is deleted — a demotion is a severity change, always visible in the report (pretty,
--format json, and--format sarifagree).--fail-on/CI gates keyed tocriticaltherefore stop triaging getter noise, whileinfofindings remain auditable. - a method of a
-
--fail-on <severity>(opt-in CI gate).sdkt auditexits0on findings by default. With--fail-on critical|warning|info(case-insensitive) the already-computed severity summary is mapped to the process exit code: exit1when any finding is at or above the threshold, after the full report has been printed unchanged — in pretty, JSON, and SARIF output alike. An invalid level is a usage error (exit2) raised before any analysis runs. The flag is independent of--disable: disabled rules produce no findings and therefore never trip the gate. -
audit --list-rulesdiscovers all registered built-in rules (with id, severity, and description). Supports--format jsonand does not require a source path argument. -
Health verdict exit codes.
sdkt healthexits non-zero when the verdict iscritical(for example, the deployed WASM does not match the file passed with--wasm), so a mismatched artifact cannot pass CI or an agent check that only reads the exit code.at_riskremains non-blocking (exit 0), matching howrelease-assurancetreats it. -
sdkt-agent. A separate binary that turns a plain-language request into a validated, read-onlysdktinvocation:sdkt-agent "check health of contract C… on testnet". It resolves the capability from the registry, pins an explicit testnet endpoint for network-touching checks, refuses mutating capabilities and mainnet targets outright, and never guesses on an ambiguous request.--format jsonemits a versioned result document; the default output is a six-line summary. Full guide — build, capability surface, ambiguity and refusal codes, artifact roles, mutation boundary, network policy, result semantics, troubleshooting: sdkt-agent. -
Agent capability registry (
sdkt_core::registry). The CLI's capabilities are described in one machine-readable table — command path, required/optional arguments, output formats, exit semantics, network requirement, safety class, and an evidence level — exportable as a versioned JSON manifest (sdkt_core::registry::to_json()). It is metadata about existing commands only: nothing is executed and no new CLI surface is added. Mutating capabilities (deploy, invoke, tx sign/submit, storage extend/restore, project deploy, identity fund) are markedrequires_confirmationand are never marked verified by a read-only audit. Adding a capability means appending oneCapabilityentry and keeping the parity test green; see the module docs for the rules the tests enforce. -
sdkt build --format json. Emits a single JSON document on stdout:{"success": true, "artifacts": [{"alias", "path", "wasm_artifact"}]}on success, or{"success": false, "error": "..."}with a non-zero exit on failure. The advisorysdkt.lockreport is written to stderr so stdout stays parseable; the default pretty output is unchanged. -
Contract metadata (
contractmetav0).sdkt wasm inspect <file.wasm>decodes thecontractmetav0custom section and prints aContract Metadatablock ofkey = valueentries (for example thersver,rssdkver, andcliverprovenance keys) in section order, with duplicate keys preserved. A WASM without the section printsContract Metadata (0):followed by(none). With--format jsonthe entries appear as the additivemetadata.contract_metaarray of{ "key": ..., "value": ... }objects — an empty array when the section is absent — alongside the existingmetadatakeys, which are unchanged. A malformedcontractmetav0payload is reported as a classified error rather than a panic. -
sdkt wasm metadata --contract <id>reports the same entries for the deployed contract, from the fetched (or cached) WASM bytes: pretty output gains the identicalContract Metadatablock, and--format jsongains an additive top-levelcontract_metaarray alongside the existing inspection keys, which are unchanged. It is empty when the deployed WASM carries nocontractmetav0. -
Mainnet safety. Mutating commands (
tx submit,invoke,deploy,project deploy) refuse to target mainnet unless you explicitly select the network — via--network-profile,--rpc-url, or--network-passphrase. A testnet-default passphrase pointed at a mainnet endpoint is rejected before any request is sent, protecting against signing for the wrong network.
sdkt fuzz campaign accepts repeatable --expect-success <FN> and
--expect-error <FN:TYPE:CODE> declarations. A function may have one
expectation; declaring it with both flags is a usage error that exits 2 and
names the conflicting function. A single valid declaration keeps its existing
campaign behavior. Malformed --expect-error declarations also remain usage
errors.
- All subcommands return
Result<(), Box<dyn std::error::Error>>. - Library-level errors (
RpcError,DecodeError,WasmError) are bubbled up to the CLI. - The CLI uses
eprintln!to print human-readable errors and exits non-zero on fatal errors to ensure correct bash piping behavior.