Config
Caatinga projects use caatinga.config.ts.
import { defineConfig } from "@caatinga/core";
export default defineConfig({
project: "my-dapp",
defaultNetwork: "testnet",
contracts: {
counter: {
path: "./contracts/counter",
wasm: "./contracts/counter/target/wasm32v1-none/release/counter.wasm",
},
},
networks: {
testnet: {
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
},
},
frontend: {
framework: "vite-react",
bindingsOutput: "./src/contracts/generated",
},
});Field reference
caatinga.config.ts has no version field (unlike caatinga.artifacts.json); do not look for one.
Root config:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
project | string (min 1) | yes | — | |
defaultNetwork | string (min 1) | no | "testnet" | |
buildRoot | string (min 1) | no | — | Cargo workspace root for a single stellar contract build |
contracts | Record<string, ContractConfig> | yes | — | at least one entry |
networks | Record<string, NetworkConfig> | yes | — | at least one entry |
frontend | FrontendConfig | no | — | optional frontend configuration (see below) |
zk | ZkConfig | no | — | ZK circuit configuration (see below) |
ContractConfig (each value in contracts):
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
path | string (min 1) | yes | — | contract source directory |
wasm | string (min 1) | yes | — | compiled WASM path |
buildFeatures | string[] | no | — | Cargo features passed to stellar contract build |
dependsOn | string[] | no | [] | contract names deployed first |
deployArgs | Record<string, string | number | boolean> | no | {} | constructor args; supports placeholders |
FrontendConfig (optional root frontend field):
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
framework | "vite-react" | no | "vite-react" | Official templates are Vite + React only; no Next.js or Astro adapter yet. |
bindingsOutput | string (min 1) | yes | — | path for generated bindings |
envFile | string (min 1) | no | — | frontend env file written by ctg sync-env |
env | Record<string, string> | no | — | maps config contract keys (or rpcUrl / networkPassphrase) to env var names |
postDeploy (optional root field):
| Field | Type | Required | Notes |
|---|---|---|---|
postDeploy | array | no | admin-signed invokes run after full deploy |
Each postDeploy entry:
| Field | Type | Required | Notes |
|---|---|---|---|
contract | string (min 1) | yes | configured contract name |
method | string (min 1) | yes | Soroban method to invoke |
args | Record<string, string | number | boolean> | no | supports the same placeholders as deployArgs |
source | string (min 1) | no | override --source for this hook (validated via assertSafeSourceAccount) |
expect | string or { matcher, value? } | no | verify stdout with string equality or structural matchers (see below) |
kind | "invoke" | "read" | no | "invoke" (default) submits; "read" simulates without signing |
Structural expect matchers: equals, reachable, isNull, isArray, minLength, maxLength, contains, matches, jsonEquals. See CLI — Expect DSL for examples.
postDeployRead (optional): same shape as postDeploy; always simulated (kind: "read"). Use a read-only identity separate from write hooks when testnet state accumulates.
smoke (optional):
| Field | Type | Notes |
|---|---|---|
smoke.reads | array | read checks for ctg smoke |
useFreshSymbol | boolean | inject ephemeral symbol arg (UUID) on each read |
Each smoke.reads / postDeployRead entry uses the same fields as postDeploy (contract, method, args, source, expect, optional kind).
When useFreshSymbol is true, Caatinga adds a symbol argument with a fresh UUID to each smoke read so testnet writes do not reuse shared keys. See Testnet hygiene.
NetworkConfig (each value in networks):
| Field | Type | Required | Notes |
|---|---|---|---|
rpcUrl | string (valid URL) | yes | |
networkPassphrase | string (min 1) | yes |
ZkConfig (optional root zk field):
| Field | Type | Required | Notes |
|---|---|---|---|
zk.circuits | Record<string, ZkCircuitConfig> | yes | at least one circuit entry |
ZkCircuitConfig (each value in zk.circuits):
| Field | Type | Required | Notes |
|---|---|---|---|
path | string (min 1) | yes | directory containing .circom files |
protocol | "groth16" | yes | only Groth16 supported today |
curve | "bls12381" | yes | only BLS12-381 supported today |
verifierContract | string | no | contract name for on-chain verification |
Example: postDeploy, postDeployRead, and smoke
From the react-vite-counter template:
postDeployRead: [
{
contract: "counter",
method: "get",
kind: "read",
args: {},
expect: { matcher: "reachable" },
},
],
smoke: {
useFreshSymbol: false,
reads: [
{
contract: "counter",
method: "get",
expect: { matcher: "reachable" },
},
],
},Address alias resolution in hook args
Method args in postDeploy, postDeployRead, smoke.reads, ctg invoke, and ctg read may use:
${source.address}— resolved from the hook--sourceor CLI--source${contracts.<name>.contractId}— resolved from artifacts- Raw CLI aliases (for example
alice) — resolved viastellar keys addresswhen the value looks like an alias (≥3 characters)
Prefer ${source.address} over raw aliases in config. Doctor prints advisory warnings for alias-like hook args. Unresolved aliases throw CAATINGA_ADDRESS_ALIAS_UNRESOLVED.
Run hooks with ctg wire, read checks with ctg smoke, or the full ctg regression pipeline — see CLI.
Artifacts
Artifacts are network-scoped so counter can have different contract IDs on testnet and mainnet. Schema v2 is current; v1 files are still readable. Run ctg migrate artifacts to bump the file version without redeploying.
Multi-frontend: one caatinga.artifacts.json per Caatinga project root. Multiple apps (web, admin, mobile wrapper) should import the same artifacts file and generated bindings — do not fork artifacts per frontend.
Multi-environment (staging vs production on the same network) is not supported yet. Options:
- Separate git branches
- Separate Caatinga projects
- Wait for a future
environmentsdimension
Top-level shape: project (string), version (1 or 2), and networks (Record<network, { contracts, dependencyGraph }>). New projects initialize with version: 2.
{
"project": "my-dapp",
"version": 2,
"networks": {
"testnet": {
"contracts": {},
"dependencyGraph": {}
}
}
}After a deploy, each contract is recorded under networks.<network>.contracts.<name> as a ContractArtifact:
{
"project": "my-dapp",
"version": 2,
"networks": {
"testnet": {
"contracts": {
"counter": {
"contractId": "C...",
"wasmHash": "...",
"deployedAt": "2026-01-01T00:00:00.000Z",
"sourcePath": "./contracts/counter",
"wasmPath": "./contracts/counter/target/wasm32v1-none/release/counter.wasm",
"dependencies": [],
"resolvedDeployArgs": {}
}
},
"dependencyGraph": {}
}
}
}ContractArtifact fields:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
contractId | string (min 1) | yes | — | deployed on-chain ID |
wasmHash | string (min 1) | yes | — | hash of the deployed WASM |
deployedAt | ISO 8601 datetime string | yes | — | |
sourcePath | string (min 1) | yes | — | |
wasmPath | string (min 1) | yes | — | |
dependencies | string[] | no | [] | resolved dependency contract names |
resolvedDeployArgs | Record<string, string | number | boolean> | no | {} | deploy args after placeholder resolution |
upgradeStrategy | "in-place" | "redeploy" | no | — | set by ctg upgrade or redeploy |
history | ContractArtifactHistoryEntry[] | no | — | prior IDs / WASM hashes (schema v2) |
ContractArtifactHistoryEntry fields (optional on each history row):
| Field | Type | Notes |
|---|---|---|
contractId | string | prior on-chain ID (same as active for in-place upgrades) |
wasmHash | string | prior WASM hash |
deployedAt | ISO 8601 | when that version was active |
supersededAt | ISO 8601 | when replaced |
reason | "upgrade" | "rollback" | "force-redeploy" | why it was superseded |
upgradeType | "in-place" | "new-contract" | in-place = same ID, new WASM; new-contract = redeploy |
See Contract upgrade for when to use ctg upgrade vs deploy --upgrade.
Multi-contract dependencies
dependsOn lists contracts that must deploy before the current contract. deployArgs may use ${contracts.<name>.contractId} placeholders, resolved from caatinga.artifacts.json after dependencies deploy:
contracts: {
token: {
path: "./contracts/token",
wasm: "./contracts/token/target/wasm32v1-none/release/token.wasm"
},
vault: {
path: "./contracts/vault",
wasm: "./contracts/vault/target/wasm32v1-none/release/vault.wasm",
dependsOn: ["token"],
deployArgs: {
tokenContractId: "${contracts.token.contractId}"
}
}
}How resolution works
Deploy order — topological sort (
resolveDeployOrder): a depth-first walk overdependsOn, marking each contractvisitingthenvisited.- Re-entering a
visitingnode throwsCAATINGA_CONTRACT_DEPENDENCY_CYCLE; the message lists the path, e.g.a -> b -> a. - A
dependsOnentry that is not a configured contract throwsCAATINGA_CONTRACT_DEPENDENCY_NOT_FOUND.
- Re-entering a
Placeholder resolution (
resolveDeployArgs): adeployArgsvalue that is a string containing${must match exactly one of:${contracts.<name>.contractId}— resolved fromcaatinga.artifacts.json${source.address}— resolved fromstellar keys address <source>at deploy/wire time- A
${...}value that does not match throwsCAATINGA_DEPLOY_ARG_PLACEHOLDER_INVALID. - The
contractIdis read fromartifacts.networks[<network>].contracts[<name>].contractId; when absent it throwsCAATINGA_CONTRACT_DEPENDENCY_ARTIFACT_NOT_FOUND(deploy the dependency first). - A placeholder still unresolved at deploy time throws
CAATINGA_DEPLOY_ARG_PLACEHOLDER_UNRESOLVED.
There is no
${env.*}and no$(...)shell interpolation. Deploy args are data passed to the Stellar CLI, not a second templating language (see ADR 0005 and ADR 0006).CLI flag derivation (
toSnakeCaseFlag/formatConstructorCliArgs): resolved args are passed tostellar contract deployafter a--separator. Each key is converted camelCase → snake*case (insert*before each uppercase letter, strip a leading\_, lowercase). For exampletokenContractIdbecomes--token_contract_id.
End-to-end: with deployArgs: { tokenContractId: "${contracts.token.contractId}" }, token deploys first, its contractId is recorded in caatinga.artifacts.json, and the vault deploy appends -- --token_contract_id C<token-id>.