Quickstart (10 minutes)
In this quickstart you write a small module with persistent state, and HiveKit compiles it to Hive Bytecode
(a .hbc file) that NDSR executes deterministically. You run it
on your machine with the same runtime the network uses, then deploy it to the testnet and call it through
validator consensus. Pick your language once; every tab on this page follows your choice.
The outputs below were recorded by running exactly these commands with ndsr 1.0.0 and the HiveKit SDKs
from the deployed testnet branch. Module addresses depend on your source code and SDK version, so yours may
differ; gas and outputs for the same artifact are identical on every machine.
1. Install the SDK
Section titled “1. Install the SDK”rustup target add wasm32-unknown-unknowncargo install --path necter-sdk/hivekit-rs # installs the `hivec` CLIRequires Go 1.21+ and TinyGo 0.42.
cd necter-sdk/hivekit-gogo build -o ~/.local/bin/hivec ./cmd/hivec # the Go `hivec` CLIRequires Node.js 18+. TypeScript modules compile with AssemblyScript (a strict TypeScript subset).
cd necter-sdk/hivekit-jsnpm ci && npm run buildnpm install -g . # installs the `hivec` CLIRequires Node.js 18+. JavaScript (and full TypeScript with --target js) runs in a JavaScript engine
compiled into the module.
cd necter-sdk/hivekit-jsnpm ci && npm run buildnpm install -g . # installs the `hivec` CLIRequires Python 3.8+.
python3 -m venv .venv && . .venv/bin/activatepip install ./necter-sdk/hivekit # installs the `hivec` CLIEach SDK ships its own hivec; install the one for your language. hivec run and hivec inspect call
ndsr (found through $NDSR_BIN, ndsr on your PATH, or tools/ndsr in a parent directory).
2. Write a module
Section titled “2. Write a module”A module exports functions. Each takes the call input (usually JSON) and returns a string. This one adds two numbers and keeps a counter in persistent storage, emitting an event on every increment.
cargo new --lib my_module && cd my_module[package]name = "my_module"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"] # rlib lets `cargo test` use the module natively
[dependencies]hivekit = { path = "../necter-sdk/hivekit-rs" }serde = { version = "1", features = ["derive"] }
[profile.release]opt-level = "s"lto = truecodegen-units = 1panic = "abort"strip = trueuse hivekit::prelude::*;
/// Exported as "addNumbers" (camelCase of the identifier).#[hive_export]fn add_numbers(input: Value) -> Result<Value, String> { let a = input["a"].as_i64().ok_or("`a` must be an integer")?; let b = input["b"].as_i64().ok_or("`b` must be an integer")?; Ok(json!({ "total": a + b }))}
/// Exported under an explicit name.#[hive_export("increment")]fn bump(input: Value) -> Result<Value, String> { let by = input["by"].as_i64().unwrap_or(1); let n = storage::get_json::<i64>("n").unwrap_or(0) + by; storage::set_json("n", &n); emit("incremented", &json!({ "value": n })); Ok(json!({ "value": n }))}
// Every exported function, once per crate.hive_module!(add_numbers, bump);mkdir greeter && cd greetermodule example.com/greeter
go 1.21
require github.com/necter-network/hivekit-go v0.0.0
replace github.com/necter-network/hivekit-go => ../necter-sdk/hivekit-gopackage main
import ( "errors"
hivekit "github.com/necter-network/hivekit-go")
type pair struct { A int64 `json:"a"` B int64 `json:"b"`}
func init() { // Typed: JSON in, JSON out; a returned error fails the call. hivekit.DefineJSON("addNumbers", func(p pair) (map[string]int64, error) { return map[string]int64{"total": p.A + p.B}, nil })
// Persistent state + an event. hivekit.DefineJSON("increment", func(in struct { By int64 `json:"by"` }) (map[string]int64, error) { if in.By == 0 { in.By = 1 } if in.By < 0 { return nil, errors.New("by must be positive") } var n int64 if _, err := hivekit.StorageGetJSON("n", &n); err != nil { return nil, err } n += in.By if err := hivekit.StorageSetJSON("n", n); err != nil { return nil, err } if err := hivekit.Emit("incremented", map[string]int64{"value": n}); err != nil { return nil, err } return map[string]int64{"value": n}, nil })}
func main() {} // required by Go, never called by NDSRgo mod tidyimport { hive, storage } from 'hivekit'
function readCount(): i64 { const cur = storage.get("count"); return cur.length == 0 ? 0 : I64.parseInt(cur);}
// increment("<by>"): add `by` (default 1), persist, emit an event, return the new count.function increment(input: string): string { const by: i64 = input.length == 0 ? 1 : I64.parseInt(input); if (by <= 0) { hive.fail("increment must be positive, got " + input); } const next = readCount() + by; storage.set("count", next.toString()); hive.emit("incremented", "{\"by\":" + by.toString() + ",\"count\":" + next.toString() + "}"); return next.toString();}
function get(input: string): string { return readCount().toString();}
hive.define("increment", increment);hive.define("get", get);AssemblyScript handlers take and return plain strings and have no built-in JSON; the
JavaScript tab runs full TypeScript/JavaScript instead.
const { hive, db } = require('hivekit')
hive.define('increment', (input) => { const by = input.by === undefined ? 1 : input.by if (!Number.isInteger(by) || by <= 0) throw new Error('by must be a positive integer') const count = (db.get('count') || 0) + by db.set('count', count) hive.emit('incremented', { by, count }) return { count }})
hive.define('get', () => ({ count: db.get('count', 0) }))from hivekit import hive
@hive.define("increment")def increment(input): by = input.get("by", 1) if isinstance(input, dict) else 1 if not isinstance(by, int) or by <= 0: raise ValueError("by must be a positive integer") count = hive.db.get("count", 0) + by hive.db.set("count", count) hive.emit("incremented", {"by": by, "count": count}) return {"count": count}
@hive.define("get")def get(): return {"count": hive.db.get("count", 0)}3. Compile to Hive Bytecode (.hbc)
Section titled “3. Compile to Hive Bytecode (.hbc)”hivec build # cargo build --target wasm32-unknown-unknown --release, ABI check, packagehivec inspect dist/my_module.hbcfile: dist/my_module.hbcmanifest_address: 0x9cf5a3ab9caa72f518a1366b913a6c48d75b9e5b07e421a6e445c377aabe6127 (verified)name: my_modulelanguage: rustcompiler: hivec-rs/0.2.0runtime: hive-wasm-v1module.wasm: 108624 bytesimports: storage.get, storage.set, hive.abort, hive.emitfunctions: 0 addNumbers 1 incrementabi: hive-wasm-v1 okhivec build . # tinygo build -target=wasm-unknown + ABI check + package{ "compiler": "hivec-go/1.0.0 tinygo/0.42.0", "functions": ["addNumbers", "increment"], "hbc": "dist/greeter.hbc", "manifest_address": "0x904ba69ab71ad216aef7cec2df0cde882f774b67219aa11b49a5c6f76928f211", "wasm_bytes": 425207}The module name defaults to the directory name (-name overrides it).
hivec build counter.ts # AssemblyScript 0.28.20, --runtime stub -O3hivec inspect dist/counter.hbc{ "abi_valid": true, "functions": [{ "func_id": 0, "name": "get" }, { "func_id": 1, "name": "increment" }], "manifest": { "functions": ["get", "increment"], "language": "assemblyscript", "name": "counter", "runtime": "hive-wasm-v1" }, "manifest_address": "0x5eb1cb756afc894e78ad6732a3980ff7c9da30b255638209a37531db70c39a09", "wasm_bytes": 7228}hivec build counter.js # embeds your script in the JavaScript engine module{ "functions": ["get", "increment"], "wasm_bytes": 3365562,}The JavaScript engine makes the module about 3.4 MB; the AssemblyScript target produces a few KB.
hivec build counter.py # → dist/counter.hbc (+ counter.manifest.json){ "functions": ["get", "increment"], "wasm_bytes": 10072513,}The pre-initialized Python interpreter makes the module about 9.6 MiB (the limit is 12 MiB).
functions is always sorted: a function’s id is its index in that list, never the order you defined them.
4. Run it locally with ndsr
Section titled “4. Run it locally with ndsr”ndsr run executes one function exactly like a node does and prints the signed receipt. With
--data-dir the module’s state persists between runs.
ndsr run dist/my_module.hbc addNumbers --input '{"a":2,"b":3}'hivec run dist/my_module.hbc increment '{"by":2}' --data-dir .nodehivec run dist/my_module.hbc increment '{"by":2}' --data-dir .node| Call | output |
gas_used |
events |
|---|---|---|---|
addNumbers {"a":2,"b":3} |
{"total":5} |
10 570 | none |
increment {"by":2} |
{"value":2} |
18 671 | incremented {"value":2} |
increment {"by":2} again |
{"value":4} |
19 635 | incremented {"value":4} |
hivec run dist/greeter.hbc addNumbers '{"a":2,"b":3}'hivec run -data-dir .node dist/greeter.hbc increment '{"by":2}'| Call | output |
gas_used |
events |
|---|---|---|---|
addNumbers {"a":2,"b":3} |
{"total":5} |
91 204 | none |
increment {"by":2} |
{"value":2} |
98 912 | incremented {"value":2} |
Go flags go before the file (-data-dir, -gas, -module). TinyGo’s runtime start-up costs roughly
35–40k gas per call.
hivec run dist/counter.hbc increment 5 --data-dir .nodehivec run dist/counter.hbc get --data-dir .node| Call | output |
gasUsed |
events |
|---|---|---|---|
increment "5" |
5 |
12 305 | incremented {"by":5,"count":5} |
get |
5 |
3 427 | none |
hivec run dist/counter.hbc increment '{"by":2}' --data-dir .node| Call | output |
gasUsed |
|---|---|---|
increment {"by":2} |
{"count":2} |
2 557 020 |
The engine start-up is pre-initialized at build time, so a small call costs about 2–3 M gas. hivec run
uses a 1,000,000,000 gas limit by default; ndsr run defaults to 1,000,000, so pass --gas 50000000 when
you call ndsr run directly.
hivec run counter.py increment '{"by": 2}' --data-dir .statendsr run dist/counter.hbc get --data-dir .state --gas 50000000| Call | output |
gas used |
|---|---|---|
increment {"by": 2} |
{"count": 2} |
about 5 M |
get |
{"count":2} |
3 559 178 |
A small Python call costs about 3.5–5 M gas. With ndsr run’s default limit of 1,000,000 the call fails
with "error": "out of gas", so pass --gas 50000000.
Every node that runs the same call on the same state gets the same receipt_hash. That is what lets
validators and miners agree. Receipts
5. Deploy to the testnet
Section titled “5. Deploy to the testnet”-
Sign in. Open testnet.necter.network and connect your wallet. Signing the Sign-In with Ethereum message costs nothing.
-
Upload the module. Go to Develop → Create, open the Work step and choose Upload .hbc. The Hub recomputes the content address and rejects any mismatch. (From a script,
POST /v1/moduleswith{"hbc_b64": "…"}and your session token or an API key does the same; see Upload and call a module.) -
Call it through validator consensus. No sign-in is needed to call a deployed module:
Terminal window curl -s -X POST https://testnet-rpc.necter.network/v1/execute \-H 'content-type: application/json' \-d '{"module":"0x<your manifest_address>","function":"increment","input":{"by":2},"gas_limit":50000000}'The Hub sends the call to every validator and answers once a quorum (3 of 4) agree on the receipt hash:
{"success": true,"output": "{\"value\":2}","receipt_hash": "0x…","gas_used": 18671,"events": [{ "name": "incremented", "data": { "value": 2 } }],"round_id": "exec:0x…","agreeing": 3,"quorum": 3,"consensus": { "state": "finalized", "validators": 4, "quorum": 3 }}State changes made this way are kept by the validators, so the next
incrementreturns4. -
Find it in the store. Paste your module address into the search box of the store’s Explorer.
6. Make it mineable
Section titled “6. Make it mineable”Calling a module through /v1/execute runs it on the validators. To have miners run your work and get
paid for it, you publish a project: a stateless worker module plus a signed manifest, a vault and a
task source. That is the next step: