Cross-module composition
The problem
Section titled “The problem”Real applications are made of parts: a ledger uses a math library, a marketplace uses a registry and a payments module, a game uses a rules module. You want to deploy them separately, upgrade by address, reuse other people’s modules and write each part in the best language for it.
Why Necter fits
Section titled “Why Necter fits”hive.call(address, function, input)calls any deployed module synchronously, regardless of the language it was written in. Modules are addressed by content hash, so you call exactly the code you reviewed.- The callee’s state changes and events are merged into yours only if it succeeds; if your call fails later, everything rolls back together. Atomic composition without locks.
- The callee only ever touches its own state; you can’t corrupt it and it can’t corrupt yours.
Example 1: a Go ledger calling a Go math module
Section titled “Example 1: a Go ledger calling a Go math module”The SDK’s ledger example calls math_module.multiply, keeps a running total and emits an event:
hivekit.DefineJSON("record", func(in recordIn) (recordOut, error) { var res struct { Result float64 `json:"result"` } if err := hivekit.CallJSON(in.Math, "multiply", map[string]int64{"a": in.A, "b": in.B}, &res); err != nil { return recordOut{}, err } // … check the product is a safe integer, add it to the stored totals … if err := hivekit.StorageSetJSON(stateKey, t); err != nil { return recordOut{}, err } if err := hivekit.Emit("ledger.recorded", map[string]int64{"product": product, "total": t.Total, "count": t.Count}); err != nil { return recordOut{}, err } receipt := hivekit.Hash([]byte(hivekit.StorageGetString(stateKey))) return recordOut{Product: product, Total: t.Total, Count: t.Count, Receipt: receipt}, nil})Locally, -module makes the callee available to hive.call:
hivec build ./examples/math_module && hivec build ./examples/ledgerhivec run -data-dir ./st -module dist/math_module.hbc dist/ledger.hbc record \ '{"math":"0x5b89b7c4f37ab4cf68f30999424ee32775d5c137d35c066e8ce9ce75563887bb","a":6,"b":7}'Recorded: {"product":42,"total":42,"count":1,"receipt":"0xecf26446…3cdd0"}, gas_used: 488464. Both modules
are deployed on the testnet with exactly these addresses (Go math_module 0x5b89…87bb, ledger
0x8c69…ce1d).
Example 2: across languages (Rust → Go)
Section titled “Example 2: across languages (Rust → Go)”The Rust counter example’s callAdd calls addNumbers on any module and stores the sum. Pointed at the Go
math module:
#[hive_export("callAdd")]fn call_add(input: Value) -> Result<Value, String> { let module = module_arg(&input)?; let out = call_json(module, "addNumbers", &json!({ "a": input["a"], "b": input["b"] })) .map_err(|e| e.to_string())?; let total = out["total"].as_i64().ok_or("callee returned no total")?; storage::set_json("last_sum", &total); emit("remote_sum", &json!({ "total": total })); Ok(json!({ "total": total, "via": module }))}
#[hive_export("tryCall")]fn try_call(input: Value) -> Result<Value, String> { let module = module_arg(&input)?; let function = input["function"].as_str().ok_or("`function` is required")?; Ok(match call_json(module, function, &input["input"]) { Ok(output) => json!({ "ok": true, "output": output }), Err(e) => json!({ "ok": false, "code": e.code(), "error": e.to_string() }), })}hivec run dist/counter.hbc callAdd \ '{"module":"0x5b89b7c4f37ab4cf68f30999424ee32775d5c137d35c066e8ce9ce75563887bb","a":1,"b":2}' \ --data-dir ./st2 --module go/math_module.hbc# → {"total":3,"via":"0x5b89…87bb"} gas_used 124133
hivec run dist/counter.hbc tryCall \ '{"module":"0x5b89b7c4f37ab4cf68f30999424ee32775d5c137d35c066e8ce9ce75563887bb","function":"divide","input":{"a":1,"b":0}}' \ --data-dir ./st2 --module go/math_module.hbc# → {"code":-3,"error":"hive.call: callee failed","ok":false} gas_used 108105The callee failed (division by zero), its effects were discarded, and the caller handled code -3 instead of
failing. On the testnet the same calls run through /v1/execute against the deployed addresses: no --module
needed, the validators resolve callees from the Hub.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
| −1 | module not found |
| −2 | function not found |
| −3 | callee failed (its writes and events discarded) |
| −5 | call depth exceeded (max 8) |
| −6 | malformed address |
Out of gas inside a callee fails the whole call (the callee had all your remaining gas).
Considerations
Section titled “Considerations”- Ordering: rounds of modules that use
hive.callare serialized against every other stateful round on the network, so heavy composition reduces parallelism. Keep pure helpers free ofstorageandhive.call. - Gas: each
hive.callcosts 10,000 plus bytes plus the callee’s gas. Interpreter-based callees (JavaScript, Python) add millions of gas each. - Trust: an address pins exact code; there are no upgradable proxies. To “upgrade”, deploy a new module and change the address your caller uses (store it in state behind your own admin rule if you need that).