cosmwasm-vm-jsrunning CosmWasm contracts in the browser
cosmwasm-vm-js is a TypeScript implementation of the host interface that runs CosmWasm contracts. It can load the same WebAssembly contract deployed to a chain and execute it in a browser or Node.js process.
I built it because every contract tool I wanted to make depended on the Rust VM and its surrounding toolchain. A browser-based simulator, debugger, or playground needed a smaller boundary. Once I wrote that boundary down, it turned out to be about fifteen host functions, a handful of required exports, and a memory convention.
That also answered a second question I was working on: what would another contract language have to produce? Rust dominated CosmWasm development because its standard library implemented the interface. WebAssembly itself did not require Rust. A second host and two experimental compilers let me test both sides of that claim.
CosmWasm contracts run against a small host interface
WebAssembly can compute and read or write its own linear memory. It cannot access files, the network, a clock, or chain state unless the host gives it a function for doing so.
Those functions are imports. In the JavaScript VM, I provide them when instantiating the module:
const imports = {
env: {
db_read: this.db_read.bind(this),
db_write: this.db_write.bind(this),
db_remove: this.db_remove.bind(this),
addr_validate: this.addr_validate.bind(this),
secp256k1_verify: this.secp256k1_verify.bind(this),
query_chain: this.query_chain.bind(this),
debug: this.debug.bind(this),
abort: this.abort.bind(this),
// fifteen in total
},
};A contract can only reach operations in that object. If the host does not supply filesystem access, the contract has no filesystem operation to call. This makes the import list the effective boundary between the contract and the chain.
The CosmWasm imports cover a narrow set of capabilities:
| Imports | Purpose | Availability |
|---|---|---|
db_read, db_write, db_remove | contract storage | always |
db_scan, db_next | storage iteration | iterator feature |
addr_validate, addr_canonicalize, addr_humanize | address validation and conversion | always |
secp256k1_verify, secp256k1_recover_pubkey | secp256k1 signatures | always |
ed25519_verify, ed25519_batch_verify | Ed25519 signatures | always |
query_chain | queries against surrounding chain state | always |
debug | diagnostic output | always |
abort | failed assertions | abort feature |
Contracts expose functions in the other direction. The host needs allocate and deallocate to move data across the memory boundary. It also expects instantiate and an interface-version marker. Application entry points such as execute, query, migrate, reply, and sudo depend on what the contract supports.
The contract owns its memory
JavaScript cannot hand a string directly to a WebAssembly function. The contract owns its linear memory, so the host asks the contract to allocate a region and then writes the encoded bytes into it:
public allocate(size: number): Region {
const { allocate, memory } = this.exports;
const regionPtr = allocate(size);
// read the region descriptor and write into module memory
...
}Calls in both directions use this convention. The host allocates a region for the input, writes serialized bytes, calls an exported entry point, and reads the returned region. Imported functions receive pointers, decode regions from contract memory, perform the host operation, and return another pointer when needed.
This memory handshake is part of the compilation target. A language can generate valid WebAssembly and still fail as a CosmWasm language if it does not export the allocator, use the expected region layout, serialize messages correctly, or expose the required entry points.
Writing those requirements as an interface separated CosmWasm from its Rust implementation. Any compiler that emits the imports, exports, memory layout, and message formats can produce a contract. Any runtime that implements the host side can execute one.
The browser became a contract runtime
The reference VM uses Rust and Wasmer. That is appropriate for chain execution, while it made lightweight developer tools harder to distribute. I wanted someone to open a page, load a .wasm contract, execute a message, and inspect the result without installing a chain or Rust toolchain.
Browsers and Node.js already include WebAssembly engines. I only needed to implement the CosmWasm-specific imports and the memory bridge around them.
I placed everything a contract can reach behind one backend:
const backend: IBackend = {
backend_api: new BasicBackendApi("terra"),
storage: new BasicKVIterStorage(),
querier: new BasicQuerier(),
};
const vm = new VMInstance(backend);backend_api handles addresses and cryptography. storage implements the database imports. querier answers requests for surrounding chain state. The VM handles WebAssembly memory, import wiring, and entry-point calls.
Swapping one backend component changes the contract's environment without changing the contract. I can use an in-memory store in a browser, a persistent store in Node.js, a recorded querier for deterministic replay, or a wrapper that logs every read and write.
That substitution point became the basis for the simulator. A contract does not need a special debugging build. The host can observe the same imports the chain would provide.
I kept the same seams as the Rust VM
Matching final outputs was insufficient for the tools I wanted to build. If the JavaScript VM organized the work around completely different boundaries, an instrument built against it would be trapped in the simulator.
I mirrored the important seams of the Rust implementation. Each imported function has a fixed wire-facing method and a replaceable operation underneath it:
db_read(keyPtr: number): number {
const key = this.region(keyPtr);
return this.do_db_read(key).ptr;
}
db_write(keyPtr: number, valuePtr: number) {
const key = this.region(keyPtr);
const value = this.region(valuePtr);
this.do_db_write(key, value);
}db_read and db_write implement the pointer convention the compiled contract expects. do_db_read and do_db_write contain the storage behavior. Logging, replay, fault injection, and alternative stores attach at that second layer.
This structure sometimes produces less idiomatic TypeScript. I accepted that cost because the JavaScript VM was also a place to develop instruments for the authoritative runtime. Keeping comparable seams made it clear where the same hook belonged in Rust.
Alternate compilers tested the target
I built two experimental contract toolchains against the interface: one in AssemblyScript and one in C++. Their purpose was concrete. A language-independent WebAssembly target should accept another producer that emits the required imports, exports, memory layout, and message encoding.
AssemblyScript worked especially well as a browser experiment. It uses TypeScript-like syntax and can compile inside a web page. Paired with cosmwasm-vm-js, it made a complete in-browser loop possible: write a contract, compile it, instantiate it, call an entry point, and inspect the resulting storage.
The experiment also exposed work that Rust's CosmWasm libraries normally hide. A new frontend needs equivalents for message types, serialization, region allocation, storage abstractions, entry-point generation, and deterministic numeric behavior. Producing WebAssembly is only the first step.
The C++ experiment reached the same target and gave me a second independent producer. I did not continue it as a contract language, so I treat it only as evidence that the host interface was language-independent. It says nothing broader about whether C++ is a good language for contract authors.
Runtime safety cannot depend on one frontend
Opening the target to more compilers changes where execution guarantees must live. A chain cannot assume every frontend inserts correct metering or avoids nondeterministic behavior. The runtime has to validate modules, constrain what they can import, account for resource use, and stop execution that exceeds its limits.
cosmwasm-vm-js accepts a gas limit as part of the VM configuration:
constructor(
public backend: IBackend,
public readonly gasLimit?: number
)That value is configuration, not enforcement by itself. Stopping a tight WebAssembly loop requires metering or interruption support in the execution engine. The design point is that the host owns this limit and applies it uniformly to every compiled language.
Language-level safety still matters. A frontend can prevent classes of authoring errors and restrict nondeterministic features. Those checks improve contracts produced by that language. The host remains responsible for protecting the chain from any module it accepts.
This distinction made the alternate-language experiments useful beyond syntax. They forced me to separate properties supplied by a compiler from properties the runtime must enforce for every producer.
The low-level imports shape every contract language
The host interface exposes storage operations, address conversion, signature checks, chain queries, debugging, and aborts. It has no primitive for balances, ownership, permissions, or typed state. Contract libraries build those concepts on top of byte keys and values:
host operation contract meaning
----------------------- ---------------------------
db_write(key, value) save this account's balance
db_read(key) load the current owner
db_scan(start, end) list every open positionThis is where most of the work in an alternate frontend lives. The compiler and its standard library must turn source-level variables, maps, authorization checks, and typed messages into the storage and serialization conventions understood by the host.
The thin interface is useful because it keeps the runtime small and language-neutral. It also means each language has to supply a substantial semantic layer before developers can write safe contracts productively.
That realization split my work into two projects. cosmwasm-vm-js implemented and instrumented the existing host interface. The language experiments explored how higher-level contract concepts compile down to that interface. Neither required changing the chain.
I document divergences next to the affected import
A second VM cannot prove that it behaves exactly like the reference implementation. Tests establish agreement on the cases they cover. Tooling built on the JavaScript VM still needs to verify critical results against the Rust VM.
I state that boundary near the top of the README. I also document differences per import:
| import | implemented | tested | notes |
|---------------|-------------|--------|------------------------------------|
| db_read | yes | yes | |
| db_write | yes | yes | |
| addr_validate | yes | yes | |
| debug | yes | yes | appends to a list instead of |
| | | | printing to the console |
| query_chain | yes | yes | |That placement makes the scope of each claim visible. A tool that only needs storage can check the storage rows. A tool that consumes diagnostic output sees the debug difference beside the function it uses.
The caveat belongs in the artifact because every simulator, debugger, and language experiment built on this VM inherits it. The JavaScript runtime is useful precisely because it is easier to embed and modify than the chain VM. It remains a development implementation, not the final authority on consensus behavior.
The host interface became the common target
cosmwasm-vm-js reduced CosmWasm execution to the parts a tool actually needs: a WebAssembly engine, the expected imports and exports, the region-memory convention, and replaceable implementations for storage, addresses, cryptography, and chain queries.
That let me run existing Rust contracts in a browser, observe every storage operation, substitute recorded chain state, and build alternate compilers against the same target. Mirroring the Rust VM's seams kept the instrumentation portable, while the README made the remaining behavioral differences explicit.
The project also clarified the work required by a smart-contract language. Emitting WebAssembly is the easy outer boundary. A usable frontend has to reproduce the CosmWasm ABI, provide typed storage and messages, define deterministic behavior, and leave resource enforcement to the host. Once those responsibilities were separated, the runtime, simulator, and language experiments could share one concrete interface.