Skip to content

Cross-module composition

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.

  • 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-go/examples/ledger/main.go (excerpt)
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:

Terminal window
hivec build ./examples/math_module && hivec build ./examples/ledger
hivec 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).

The Rust counter example’s callAdd calls addNumbers on any module and stores the sum. Pointed at the Go math module:

hivekit-rs/examples/counter.rs (excerpt)
#[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() }),
})
}
Terminal window
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 108105

The 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.

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).

  • Ordering: rounds of modules that use hive.call are serialized against every other stateful round on the network, so heavy composition reduces parallelism. Keep pure helpers free of storage and hive.call.
  • Gas: each hive.call costs 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).