This file documents the conventions and workflow expected of any AI agent
contributing to nexus-sdk. Read it once before touching code; refer back to
it when a pattern is unclear.
nexus-sdk/ cargo workspace root
├── sdk/ `nexus-sdk` crate — Rust SDK
│ └── src/
│ ├── nexus/ high-level action types, Scheduler and Task handles,
│ │ NexusTransaction, NexusClient, Crawler, Signer,
│ │ gas pool, EventPoller, and errors
│ ├── scheduler/ Task, Schedule, occurrence, receipt, and snapshot
│ │ domain types
│ ├── transactions/ PTB builders: tap.rs, tool.rs, workflow.rs,
│ │ scheduler/, gas.rs, network.rs, tool_cashier.rs
│ ├── types/ typed on-chain object/event models
│ │ (TapRegistry, TapExecutionPayment, Tool, ToolRef,
│ │ DagExecution, NexusObjects, derive helpers, …)
│ │ plus Move-JSON / BCS serde parsers
│ ├── idents/ Move package/module/function/struct identifiers
│ │ (primitives, sui_framework, workflow, tap, move_std)
│ ├── events/ event parsing
│ ├── test_utils/ sui_mocks (gRPC mocks), nexus_mocks (client mocks),
│ │ containers (sui/redis testcontainers), faucet
│ ├── onchain_schema_gen/ Move-introspection schema generation
│ ├── signed_http/ application-layer Ed25519 signatures (feature)
│ ├── walrus/ Walrus storage client
│ ├── tool_fqn.rs ToolFqn + `fqn!` macro
│ └── lib.rs top-level re-exports
├── cli/ `nexus-cli` crate — `nexus` binary
│ └── src/
│ ├── main.rs top-level Cli + Command dispatch
│ ├── prelude.rs common imports (GasArgs, JSON_MODE, sui)
│ ├── display.rs command_title!, notify_success!, item!, loading!,
│ │ json_output(), JSON_MODE: AtomicBool
│ ├── sui.rs get_nexus_client(), gRPC client helpers
│ ├── cli_conf.rs CliConf (~/.nexus/conf.toml)
│ └── {tool,conf,dag,task,gas,tap,completion}/mod.rs
│ subcommand groups with their handlers
├── toolkit-rust/ Rust toolkit (`nexus-toolkit`) for tool authors
├── helpers/ workspace helper crates / just recipes
├── docs/ gitbook-synced docs (cli.md is the CLI reference)
├── target/ cargo build output
├── Cargo.toml workspace manifest
├── rustfmt.toml unstable nightly-only options used
├── .nightly-version pinned nightly toolchain for fmt
├── rust-toolchain.toml stable toolchain for everything else
├── STYLE_GUIDE.md markdown style rules (markdownlint-enforced)
├── CONTRIBUTING.md contributor guide (pre-commit, commits, PRs)
└── CHANGELOG.md keep-a-changelog, per-crate sections
Sibling repos checked out next to this one (paths depend on local layout):
nexus/— onchain Move packages (sui/primitives,sui/interface,sui/tool,sui/registry,sui/workflow,sui/scheduler), example TAPs (sui/examples/demo_tap), and the off-chain leader (be/leader/). Itssui/bin/publish.shandsui/bin/test_demo.share the canonical localnet bring-up and demo driver.nexus-tools/,nexus-workbench/,nexus-api/— sibling repos consumed by docker-compose workbenches.
- High-level actions live in
sdk/src/nexus/<area>.rs. Each action takes&selfon the*Actionsstruct (held byNexusClient), submits via the shared signer/gas/crawler, and returns a typed*Resultstruct. Free- functionfetch_*helpers (e.g.fetch_registry) live in the same file when they're useful without a full client. - Scheduling uses the neutral domain in
sdk/src/scheduler, stateful handles insdk/src/nexus/scheduler, and one compiler insdk/src/transactions/scheduler. Stateful calls begin atNexusClient::scheduler; custom programmable transactions begin atNexusClient::transaction. - PTB builders live in
sdk/src/transactions/<area>.rsand take&mut TransactionBuilderplus&NexusObjects. They never read from the network; they only emit move calls/inputs. Pair an action with one PTB builder per logical transaction. - Errors flow through
NexusError(Wallet,Configuration,TransactionBuilding,Rpc,Parsing,Timeout,Channel,Storage). Pick the variant that matches the root cause; onlyConfigurationis string-typed. - On-chain decoding: prefer
crawler.get_object::<T>(JSON) for SDK types that implementDeserializeagainst Sui's Move-JSON, andcrawler.get_object_contents_bcs::<T>for objects whose layout is best decoded from raw BCS. Dynamic field readers (get_dynamic_fields_bcs,get_dynamic_object_fields) sit on top of both. - ID derivation uses
derive_tool_id,derive_tool_cashier_id,derive_walk_execution_event_task_id, etc. Never reimplement the ascii-string / BCS-blake2b derivation in shell or Python. - Struct reuse comes before new structs. Before adding any new Rust struct, inspect the existing structs in the related SDK/CLI/type module and confirm that none of them, and no reasonable modification of them, can satisfy the new purpose. If a new struct is still necessary, add a short doc comment or nearby comment that states exactly what is missing from the closest existing struct and why modifying that existing struct would be wrong.
The Move binding refresh is not type only. It refreshes the committed
canonical package IR for each Nexus Move package. The reduced framework support
IR remains pinned to the SDK. The generated Rust surface in
sdk/src/move_bindings is the ABI boundary for Move types, type tags, BCS and
serde implementations, and typed call targets. Rust domain modules may add
helpers on top, but they should not duplicate Move ABI logic.
How the pipeline fits together:
- Refresh half (on demand through
just sdk rebind):sdk/src/bin/regenerate_bindings.rsfetches each Nexus package's normalized IR throughtalus_sui_move_codegen::fetch_package, replaces concrete Nexus package identities with stable SDK binding slots, normalizes the unused deployment version, extracts authoritative protocol limits from matching Nexus Move source, and writes committed JSON undersdk/src/move_bindings/ir/<package>.jsonplussdk/src/move_bindings/protocol_limits.toml. The reducedmove_stdandsui_frameworkIR files remain unchanged and are rendered alongside the six refreshed Nexus package files. - Offline half (every build):
sdk/build.rsreads the committed IR and renders one$OUT_DIR/<package>_types.rsfile per package. The rendered Rust includes generated Move structs, enum variants, type tags, serde and BCS implementations, and function call targets. The rendered files are never committed. - Wiring:
sdk/src/move_bindings/mod.rsscopes generated packages throughwith_nexus_scope, includes the rendered output, and exposes extension modules fromsdk/src/move_bindings/extensions. Runtime PTB builders call generated*_targetfunctions instead of hand written package, module, and function strings.
Key invariants:
- Framework packages are scoped to their fixed addresses:
move_stduses0x1, andsui_frameworkuses0x2. - Normal Nexus regeneration does not rewrite framework IR. Update that support surface explicitly only when the pinned Sui version changes.
- Committed Nexus IR uses stable binding slots:
primitivesuses0xa1,interfaceuses0xa2,tooluses0xa7,registryuses0xa3,workflowuses0xa4, andscheduleruses0xa5. These slots describe the canonical SDK package graph and are not deployment package IDs. The unused package object version is normalized to1. - Nexus package call targets use the current package ids from
NexusObjects. Type identity uses the defining package id where Sui upgrades keep type tags pinned to the original package. - Generated binding output is derived from committed IR. If a Move function, struct, enum, field, or signature changes, regenerate the IR and fix Rust call sites against the generated compiler errors rather than patching generated output by hand.
Run this after the on chain Move in Nexus changes identifiers, signatures, functions, datatypes, or protocol-limit constants. The preferred command takes the Nexus sui source directory, derives its deployment objects TOML, and uses the default local Sui gRPC endpoint:
just sdk rebind {your_path_to_nexus_contracts}You can pass the gRPC endpoint as the second argument when it is not http://127.0.0.1:9000:
just sdk rebind {your_path_to_nexus_contracts} http://127.0.0.1:{grpc_port}Network package metadata does not contain Move function parameter names or named constant values. When passing a deployment objects TOML instead of the source directory, pass its matching Move source root as the third argument:
just sdk rebind \
{your_path_to_objects_toml} \
http://127.0.0.1:{grpc_port} \
{your_path_to_nexus_contracts}The recipe runs sdk/src/bin/regenerate_bindings.rs with the binding_codegen feature. The binary reads package ids from the supplied deployment manifest, fetches normalized Nexus package metadata over gRPC, overlays trusted source parameter names, extracts required protocol limits, writes sdk/src/move_bindings/protocol_limits.toml, and writes the refreshed Nexus JSON under sdk/src/move_bindings/ir/. The reduced move_std and sui_framework IR files are not fetched or rewritten. Source input does not replace signatures, types, or abilities from the network.
Before writing, regeneration replaces every current and original Nexus package
ID, including cross package type references, with its stable SDK binding slot.
It also normalizes the package object version, which is not consumed by the
renderer. The concrete deployment remains authoritative for the fetched ABI,
while redeploying the same ABI does not change the committed IR. Runtime
NexusObjects supplies current package IDs for calls and original package IDs
for type identity. The recipe does not start Sui, publish packages, or run
cargo check; run the normal SDK checks after regeneration.
The committed artifacts to review are the JSON diff under sdk/src/move_bindings/ir/ and the generated sdk/src/move_bindings/protocol_limits.toml. sdk/build.rs renders Rust bindings and protocol-limit constants from those artifacts during normal builds, so a dropped or renamed generated target should be treated as an on chain API move and fixed at the call site.
- Module layout per command group (
cli/src/<group>/mod.rs):mod tap_xxx;for each subcommand handler file.use { … }block bringing handler functions and SDK result types into the module scope.#[derive(Subcommand)] enum <Group>Command { … }with one variant per subcommand; each#[command(about = …)]annotation feeds--help.- Optional nested
enum <Sub>Command { … }for two-level groupings (e.g.EndpointCommand,PaymentsCommand). pub(crate) async fn handle(command: <Group>Command) -> AnyResult<(), NexusCliError>dispatcher that destructures each variant into a flat handler call.
- Per-subcommand handler (
cli/src/<group>/<group>_<command>.rs):command_title!("…")for human progress.let nexus_client = get_nexus_client(sui_gas_coin, sui_gas_budget).await?;(omit gas args for read-only commands).- Drive the SDK action and unwrap the result.
notify_success!for human feedback.json_output(&<group>_<command>_result_json(&result))?;— JSON output is stable and consumed by scripts; keep keys flat and snake_case.
- JSON-shape helpers (
<group>_<command>_result_json) live next to their handler. They take typed SDK results and emitserde_json::Valuewith the keys the CLI documents. Always cover them with a unit test that asserts each top-level key — this catches accidental field renames. - The global
--jsonflag is stored inJSON_MODE: AtomicBool(seecli/src/prelude.rs). Interactive prompts and progress spinners must checkJSON_MODE.load(Ordering::Relaxed)and short-circuit when set.
- Unit tests sit next to their implementation in
#[cfg(test)] mod tests.- Bring the parent module in with
use super::*;and supplement withcrate::{fqn, sui::traits::*, test_utils::{nexus_mocks, sui_mocks}}plustonic::Statuswhen you need to fake gRPC errors. - Use
sui_mocks::grpc::MockLedgerService,MockTransactionExecutionService,MockSubscriptionServiceto expect gRPC calls. - For full transaction flows use
mock_execute_transaction_and_wait_for_checkpointand pass the response objects your handler depends on. - Use
sui_mocks::grpc::mock_server+nexus_mocks::mock_nexus_clientto materialise an end-to-end client against the mocked server. mock_get_object_metadata/mock_get_object_json/mock_get_object_bcs_forcover the three read paths; return a tonicStatus::not_founddirectly for "object missing" cases.
- Bring the parent module in with
- CLI dispatch tests in
cli/src/tap/mod.rs::testsand similar verify every clap variant reaches a local boundary (e.g. missing-RPC error) before any network call. Add a new arm whenever you add a subcommand. - JSON-shape tests for
*_result_jsonhelpers assert each documented top-level key withassert_eq!(json["x"], …).
- Avoid unnecessary and extraneous comments around self explanatory code. Code should be written in such a way that it doesn't require a sea of comments in the first place.
- Add
//!brief module descriptions to each module. Highlighting what the responsibility and purpose of that module is. Update this comment after any changes made to each module. - Prefer doc-comments
///to inline comments//where possible - Only use inline comments to clarify potentially confusing logic within functions. These comments should be concise (1-2 lines maximum).
///doc comments can be more verbose if the struct or the function require it.
-
Add the SDK primitive under
sdk/src/nexus/<area>.rs:- Define the params and result structs near the existing ones.
- Add the
impl <Area>Actions { pub async fn … }method. - If a new PTB shape is required, extend
sdk/src/transactions/<area>.rswith a builder that takes&mut TransactionBuilderand the relevantNexusObjectsrefs/idents. - Reuse existing typed models (
sdk/src/types/<area>.rs) and identifier constants (sdk/src/idents/<package>.rs) — only add new types when an on-chain shape genuinely changed.
-
Wire the SDK helper into the CLI:
- Create
cli/src/<group>/<group>_<command>.rswith a handler that calls the SDK and a*_result_jsonhelper. - Add a
modline and import incli/src/<group>/mod.rs. - Add a
#[derive(Subcommand)]variant (or a nestedenum) with clap attributes that match the SDK's parameter types. - Route the variant in
handle(command: <Group>Command).
- Create
-
Add tests:
- SDK: at minimum one happy-path mock test and one failure-mode test
per branch in the new function. Cover the gRPC error path with
Status::not_foundand the parse-failure path with a constructed bad response. - CLI: dispatch test that the new variant reaches the missing-RPC
boundary, plus a JSON-shape unit test for each new
*_result_json.
- SDK: at minimum one happy-path mock test and one failure-mode test
per branch in the new function. Cover the gRPC error path with
-
Update documentation:
docs/cli.md— add a command block in the existing### nexus <group>section. Follow the established**\nexus … [--flag ]`**header style and the{% hint style="info" %}…{% endhint %}` callouts.CHANGELOG.md— append bullets to## [Unreleased]under the matching### nexus-cli/### nexus-sdk/### nexus-toolkit/### docssections;#### Added,#### Changed,#### Fixed,#### Removedare the allowed verbs.
-
Verify (in this order):
just pre-commit cargo-check # cargo check --locked --workspace --bins --examples just pre-commit cargo-nextest-run # cargo nextest run --locked --fail-fast … (needs docker) just pre-commit cargo-clippy # cargo clippy --locked --all-targets --all-features just pre-commit cargo-nightly-fmt # cargo +<nightly> fmt --all --check
The fmt step is required:
rustfmt.tomluses several unstable options (imports_granularity,group_imports,reorder_impl_items, …) that the stable rustfmt rejects.cargo-nightly-fmtresolves the pinned nightly from.nightly-version(currentlynightly-2025-01-06) automatically; install it withrustup toolchain install "$(cat .nightly-version)" --component rustfmtif you don't have it yet. -
Run the equivalent
justrecipes when in doubt — they wrap the above with the right toolchain selection:just sdk check && just sdk test && just sdk fmt-check just cli check && just cli test && just cli fmt-check just pre-commit cargo-nightly-fmt
A change is "done" only when all of the following pass:
cargo +stable checkandcargo +stable testfor every touched crate (-p nexus-sdk -p nexus-cliat minimum).cargo +nightly fmt --all --check(use the pinned.nightly-version).- New public SDK items have rustdoc that explains the why, not only the what; non-obvious branches (timeouts, idempotency, error mapping) get a one-liner.
- New CLI subcommands are listed in
docs/cli.mdwith the same flag ordering and naming as--help, and have a corresponding bullet inCHANGELOG.mdunder[Unreleased]. - Pre-existing tests still pass — keep an eye on flaky CLI tests that
share
HOME/$SUI_RPC_URL/$SUI_PK; they useserial_test::serialfor a reason, never run them concurrently without that attribute.
- Read STYLE_GUIDE.md for markdown rules
(markdownlint-enforced) — ordered list items use
1.for every entry (MD029 style1/1/1); emphasis uses*asterisks*, not underscores (MD049). - Read CONTRIBUTING.md for commit-message conventions
(Conventional Commits, imperative tense) and the pre-commit hook setup
(
./.pre-commit/pre-commit --install). - The onchain Move source lives in the sibling
nexusrepo — cross-check struct layouts, function signatures, and idents there before guessing.