← writing

Building Terra's local smart-contract development stackTerra developers had no quick way to start, run, inspect, and reset a contract project. I built a project scaffold, a complete local network, and node-level tracing around that loop.

system design6 min

A project scaffold, local ledger, and inspection tool form a repeatable development loop.

When I started working on Terra's developer tools, building a contract required too much setup and too much patience. New projects began as copies of sample repositories. Testing meant connecting to a shared network or assembling a private one by hand. Debug output depended on code added to the contract. After a failed transaction, getting back to a clean state was another job.

I wanted one local loop: create a standard project, start a complete Terra network, deploy the contract, inspect what it did, wipe the state, and run it again.

I built that loop in three parts. LocalTerra ran a private Terra network with its wallet, data service, and block explorer already connected. A modified node exposed contract diagnostics at the runtime boundary. The Houston project scaffold standardized contract structure, tests, deployment scripts, and generated documentation.

LocalTerra made the chain disposable

A production blockchain preserves history across many machines. That is the behavior users need and the opposite of what I needed during development. I wanted to run a broken transaction repeatedly from the same starting state.

LocalTerra runs the network in containers and keeps chain state in a disposable volume. Resetting it is destructive and intentionally simple:

docker-compose rm -f -s -v
docker volume rm localterra_terra
docker-compose build --no-cache

Removing the ledger returns every module, account, and contract to its initial state. I could seed a test scenario once, run an experiment, inspect the result, reset, and repeat.

A local ledger returns from an experiment to its seeded starting state.
Disposable chain state lets each experiment begin from the same conditions.

A bare node would not have been enough. Terra applications reached the chain through a wallet, SDK endpoints, a data service, and an explorer. LocalTerra started those pieces together and connected them to the same private network.

The node published the same endpoints used by normal development tools:

terrad:
  ports:
    - "1317:1317"    # REST used by SDKs
    - "9090:9090"    # gRPC
    - "26657:26657"  # Tendermint RPC used by explorers

Anything a wallet, SDK, or explorer could observe through those interfaces needed to behave like Terra. Consensus across independent machines, public peer discovery, and a durable ledger could be simplified because local applications did not depend on them.

An application uses matching interfaces on local and public chain environments.
The local environment preserves the interfaces applications already use.

I used Ganache as the product reference. Ganache ran a private Ethereum chain in a box. LocalTerra could not reuse its code because Terra used a different chain and virtual machine, but it could offer the same development experience: one command starts an ecosystem that behaves like the public platform from the application's point of view.

CosmWasm gave me one place to observe every contract

Terra contracts run on CosmWasm. Contracts compile to WebAssembly and receive a small set of host functions for storage, address conversion, signatures, chain queries, and diagnostics:

db_read              addr_validate          secp256k1_verify
db_write             addr_canonicalize      secp256k1_recover_pubkey
db_remove            addr_humanize          ed25519_verify
db_scan                                     ed25519_batch_verify
db_next              query_chain            debug
                                            abort

Those functions are the only route from a contract to the surrounding chain. That made them the right place to add development instrumentation.

Logging inside a contract only works for code I can edit. It also changes the binary I am trying to inspect. Logging inside the node works for every contract executed by that node, including contracts written by someone else.

The debug import already provided the boundary:

debug(messagePtr: number) {
  const message = this.region(messagePtr);
  this.do_debug(message);
}

I modified the local runtime to collect and expose those messages. Contracts continued to call the standard CosmWasm import, so they required no LocalTerra-specific API. The node decided where the output went.

The same approach extended beyond explicit debug statements. Storage reads, writes, removals, and scans also cross host imports. Instrumenting those calls let the development runtime report what a contract actually touched, independent of its source language or internal abstractions.

A host boundary records storage calls while the contract remains unchanged.
Instrumenting host imports exposes storage activity without changing the contract.

Keeping the instrumentation at the VM boundary gave it complete coverage over locally executed contracts. It also kept the contract binary closer to what would run on the real chain.

The wallet and explorer were part of the product

LocalTerra originally looked like a node-distribution problem. In practice, developers were building applications, and their applications used more than the node.

A wallet had to recognize the local chain, submit transactions to it, and display the resulting account state. The explorer had to read the local RPC endpoint and show blocks and transactions. The data service had to index the same network the wallet was using.

I treated those connections as part of LocalTerra instead of post-installation instructions. The system came up with funded accounts and compatible service configuration. A developer could open the wallet extension, connect an application, submit a transaction, and inspect it in the explorer.

This is what made resettable state useful. Resetting a node that the rest of the stack cannot reach only tests contract execution in isolation. Resetting the complete application environment tests the path a user will actually take.

Wallet, explorer, and data service share one configured local network.
The complete application stack connects to the same resettable network.

I standardized the project before automating it

The other half of the loop began before LocalTerra started. Developers needed a predictable way to create and organize a contract project.

My earlier tooling was a collection of TypeScript scripts for common tasks. It automated commands without defining how a project should be structured. New contracts still began by copying examples, and teams made independent decisions about source layout, tests, deployment, and documentation.

That flexibility created repetitive correctness risks. Adding one query required edits in three places:

// 1. declare the message
pub enum QueryMsg {
    Config {},
    AssetConfig { asset_token: String },
}

// 2. route the message
match msg {
    QueryMsg::Config {} => to_binary(&query_config(deps)?),
    QueryMsg::AssetConfig { asset_token } => {
        to_binary(&query_asset_config(deps, asset_token)?)
    }
}

// 3. implement the handler
pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    ...
}

The compiler could verify each piece individually while missing that I had declared a message and forgotten to route it. A generator could remove that duplication only if it knew where message definitions and handlers lived.

Houston introduced a standard workspace:

contracts/
  contract1/
    src/
      lib.rs
      contract.rs
      tests.rs
    Cargo.toml

integration-tests/
  test_XXX.rs

docs/

scripts/
  deploy.rs
  migrate.rs

Cargo.toml

cargo houston new contract <dir-name> created this structure. Each contract was a Rust crate. Unit tests lived beside its code. Integration tests lived at the workspace root because they exercised deployed contracts through the chain. Generated documentation went into docs/. Deployment and migration were executable Rust programs in scripts/.

The scaffold saved some typing, but consistency was the larger benefit. I could open an unfamiliar Houston project and know where its contracts, integration tests, deployment logic, and generated artifacts lived. Tools could make the same assumption.

A standard project layout gives contracts, tests, scripts, and documentation predictable locations.
A fixed workspace lets developers and tools rely on the same structure.

A fixed structure made generation reliable

Once the project layout and message definitions had stable locations, Houston could generate the repetitive layers around a contract.

The message types already described the public contract interface. Tooling could use them to generate reference documentation, client bindings, and an application interface connected to the Terra wallet. Adding a query in the contract became the source change; the other representations could be regenerated from it.

I also kept deployment inside the repository as code:

scripts/deploy.rs
scripts/migrate.rs

This made deployment reviewable and repeatable. A README command can drift when a flag or contract address changes. A program imports the same project configuration as the rest of the toolchain and fails visibly when its assumptions stop compiling.

Documentation followed the same approach. Command-based guides were easier to test than screenshots of a web interface. I could run a sequence of commands end to end and catch a broken flag, path, or output format. The guide remained useful before a graphical interface existed and stayed verifiable afterward.

A command-based guide is executed as a sequence and checked against its results.
Runnable guide commands make broken paths, flags, and outputs visible.

The tools formed one development loop

The scaffold and local network solved different abandonment points.

Houston handled the first twenty minutes: create the workspace, add a contract, run unit tests, and generate the surrounding files. LocalTerra handled the repeated loop: start the ecosystem, deploy, execute, inspect, reset, and execute again. Node-level tracing added visibility without requiring developers to modify the contract they were studying.

The pieces fit because they shared concrete interfaces. Houston produced deployment scripts and contract binaries. LocalTerra exposed the standard Terra endpoints those scripts used. The wallet and explorer connected through the same interfaces as they did on a public network. The modified CosmWasm host observed contracts through imports the contracts already called.

I did not need a new execution model or a special debug version of every application. I needed to package the existing boundaries into a development environment and decide which layer owned each feature.

The final workflow was direct: generate a known project structure, run it against a complete local Terra network, inspect execution from the host, destroy the ledger, and start again. That turned contract development from a sequence of setup tasks into a loop I could repeat without losing the thread of the bug I was trying to fix.