Manifests and tx tooling
program_manifest! one-liners, manifest generation from source, and the no-Node hopper tx send / tx explain loop.
Hopper's tooling loop is pure Rust end to end: the program exports its
schema in one line, the CLI generates the manifest from source, and the
same CLI sends instructions and decodes confirmed transactions by name,
no npx, no node_modules, no JS toolchain anywhere.
program_manifest!: the one-liner
Every Hopper program can export its full schema as a static:
hopper::program_manifest! {
program = vault_program,
layouts = [Vault],
events = [DepositReceipt],
}
name / version / description default from the crate's
CARGO_PKG env consts. The macro glues together metadata the macros
already emit: per-layout manifests with real field types and
header-relative offsets, per-handler instruction descriptors (typed
args, account roles, strict_writes / write ranges /
mutation_complete / lamport accounts, whether a receipt is expected),
and per-event descriptors with payload-relative field offsets. Those
are the SAME generated consts the runtime enforces, so the published
manifest equals the enforced contract by construction, not by a
build step you have to remember to re-run.
The proof of the loop: the counter example's 82-line hand-assembled
manifest block deletes into the 4-line macro block, and the regenerated
manifest's layoutId is byte-identical to the checked-in one, the
enforced identity const flowing through, not a re-derivation.
hopper compile --emit manifest: generate from source
hopper compile --emit manifest --package my-program
Closes the same loop anchor idl build does, without the Node leg: it
builds a scratch harness against the target package, prints the
package's PROGRAM_MANIFEST, and writes hopper.manifest.json next to
the package (--out to override), with a friendly error naming the
expected shape when the package exports no manifest static. The
generated file immediately drives --emit ts (and the other client
generators, see Client generation) unchanged.
Both manifest dialects (the older snake_case files and the rendered
camelCase output) are accepted by the loader, additively. Old files
keep working.
The manifest carries the program's byte-range write-set end to end:
writeRanges, strictWrites, mutationComplete, lamportAccounts,
and cuEstimate. One honest note on cu_estimate: it is
author-supplied and must come from a measured worst-case run of the
actual instruction, not from summing the per-primitive tables (see the
CU costs page).
hopper tx send: the generic no-Node sender
The instruction sender stock tooling lacks (devs usually shell out to JS scratch scripts):
hopper tx send --program <program-id> \
--account payer:sw --account <vault>:w \
--data 015a00000000000000 \
--keypair ~/.config/solana/id.json --rpc https://api.devnet.solana.com
- Ordered
--account <pubkey|payer>[:s][:w]metas, one per slot; the literal spellingpayerresolves to the fee payer. - Hex
--data, local keypair signing, and refuse-before-send signer coverage: it will not broadcast a transaction whose declared signers it cannot sign. --dry-runpreviews the exact instruction plan and signer coverage; after confirmation it fetches and prints the measured CU and fee. A send you cannot budget from is half a tool.
It rides the same signed-send stack as hopper publish-idl (the
pure-Rust IDL publisher to the SPL Program Metadata PDA, no JS
toolchain, though not yet devnet-battle-tested).
hopper tx explain: named decodes from confirmed transactions
hopper tx explain <signature> decodes what a transaction actually did:
- Touch maps from the log stream:
W slot 1 (vault) [48..56) -> Vault.balance, see Self-describing transactions. - Self-CPI events from inner-instruction metadata: the
indexer-grade read path, because inner instructions survive the log
truncation that motivates
event_cpiin the first place. Authenticity is judged honestly: a marker CPI whose target equals the enclosing program in a successful transaction was sink-authenticated on-chain (only that program'sinvoke_signedcan sign its event authority); a marker-shaped CPI to a foreign program is labeled exactly that. - With the program's manifest, events render named and field-decoded: little-endian integers by declared wire type, hex fallback for unknown types, manifest/payload mismatches reported, never padded over.
The loop, run live
On 2026-07-10 every instruction of the devnet smoke program was fired
with hopper tx send and decoded back with hopper tx explain: five
instructions, live CU from 762 to 3,586, including the named event
decode event: DepositReceipt (tag 0x02) { balance: 1500000, deposit_count: 3 } whose live cost matches the Mollusk lab exactly.
The full table with signatures is on the Benchmarks
page.
