Realm
blog:from-maketx-run-to-templated-contracts
gno.land/r/moul/blog:from-maketx-run-to-templated-contracts
Render
your run script is a shell script, compile it
2026-09-16 · #msgrun · #msgcall · #gas · #contracts
You write a little .gno file, you run it, it does exactly what you wanted.
gnokey maketx run -broadcast -chainid pearl-1 mykey script.gno
Then you point the same command at mainnet and it stops. Not a compile error,
not a panic: the transaction never gets to run at all. The gate is
vm.run_submitters, an allowlist of addresses permitted to send MsgRun. Empty
means off, list one address and it is on, and mainnet has it on.
why it is gated, which tells you what to do about it
MsgRun type-checks and executes arbitrary source immediately, and it is the
only code-bearing message with no other gate: MsgAddPackage clears a namespace
check and a CLA check, while MsgRun's path is forced to /e/<caller>/run and
has no namespace to check against.
MsgAddPackage publishes code someone can look at, under a name someone owns.
MsgRun publishes code that executes once and leaves no reviewable artifact. On
a chain that wants a review surface, only one of those stays open.
a run script is a shell script, structurally
You hand the chain source, it compiles, runs main(), throws the binary away.
Nothing persists, nothing is named, nothing can be called again. That is
sh script.sh with a blockchain for a kernel, and two properties follow.
A run script carries things a call cannot. MsgCall marshals arguments from
strings and accepts primitives, byte arrays and byte slices; everything else
hits panic("unexpected type in contract arg"). A script has no such limit
because it does not marshal the argument, it writes the code that builds it.
That is why GovDAO proposals are MsgRun-only.
A run script runs as you. The ephemeral path is
gno.land/e/<your-address>/run and address derivation special-cases it, so
inside a run script "the calling realm" is you. Any API resolving an actor
from its caller acts on your own account. From a deployed realm the same call
acts as that realm. This is the property people build on without noticing, and
the one that does not survive the migration.
so the migration is not a workaround, it is compiling
A run script is a shell script; a realm is the binary you ship once the script
stops changing. Move the logic on chain once and what crosses the wire
afterwards is primitives again, which is all MsgCall ever needed. Every script
I have looked at falls in one of three piles.
It only needs a func value or a struct. A wrapper realm supplies the closure, the user passes strings. Most projects did this already without framing it that way: a router realm is this fix.
It needs to act as the signer. This is the pile that bites, and I tried to
build the wrong fix for it first, a generic hub that moves an allowance on
anyone's behalf. It cannot exist, on purpose: ImpersonateTeller is on the
private ledger, so a helper realm acts only as itself and a hub acting as its
caller is a confused deputy. The fix is the owning realm exposing the entry
point, which for a token is the standard wrapper set:
var userTeller = ledger.CallerTeller() // actor = whoever crossed in = the signer
func Approve(cur realm, spender address, amount int64) {
checkErr(userTeller.Approve(0, cur, spender, amount))
}
It is a one-off. Migrations, inspections, "let me just try this". Keep using
maketx run, locally and on testnets where the gate is off. The mistake is
shipping a run script as a product surface, where every user signs arbitrary
source every time they press a button.
it is also half the gas
A front-end I looked at sends one transaction per user action, and every one
carries a MsgRun to grant an allowance and another to revoke it, around the
real call. The hygiene is genuinely nice. The MsgRun is not cleverness, it is
that the generic registry path was the only one their UI had to write once and
reuse per asset. Every token involved already exposed Approve on its own
realm, so the rewrite is mechanical. On that same chain, a single-message
approve and a single-message registry write cost:
| form | gas used |
|---|---|
MsgCall wugnot.Approve(router, 10000000) | 6,089,073 |
MsgRun with one registry write | 12.2M to 15.8M (10 samples) |
Roughly 2x, measured 2026-09-16, a cost class rather than a byte-identical
A/B since no solo MsgRun approve exists on that chain to pair. The overhead is
structural: MsgRun pays ValidateMemPackage, TypeCheckMemPackage and
per-byte preprocess gas on every transaction, MsgCall pays none of it because
the code is already on chain and already type-checked. You compile once instead
of once per user per click. Tx.msgs is a list, so the sandwich stays one
transaction and one signature: you swap message kinds, not the flow.
where this stops
GovDAO proposals stay MsgRun-only, because a proposal request carries a func
value. That is why the allowlist must never be emptied on a live chain: it would
take governance with it and leave no in-band repair. A token nobody wrapped is
still unreachable, so ask for the wrappers at listing time, not after. And local
development does not change at all: gnodev, testnets and your own chain leave
run_submitters empty.
The rule I kept out of all this is short: if a person will do it more than
once, it is a function on a realm. maketx run is for the first time you do
something, not the thousandth.
If you have a run script you think cannot be compiled, send it to me. Pile 2 is the one worth arguing about and I have only seen my own corner of it.
Transactions
| Hash | Block | Time | Msg | Caller | Status |
|---|---|---|---|---|---|
| D275522CDC…C64DA812 | 408,742 | 12d ago | Call Set | g1manfre…dlf5 | ok |
| 7FA3FBE10E…8F7C98D1 | 406,149 | 12d ago | Call Set | g1manfre…dlf5 | ok |
| F87AED9E85…1694ECE2 | 403,367 | 12d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 309AED3658…5B81E495 | 400,677 | 12d ago | EnablePkg | — | ok |
| 427251B5E8…73DED251 | 400,670 | 12d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 4A96C1BA23…1430498E | 400,668 | 12d ago | AddPackage | — | ok |
| 7D22BA87AF…F2F833BB | 271,760 | 17d ago | Call Set | g1manfre…dlf5 | ok |
| AE4BBD069E…76165AA9 | 271,476 | 17d ago | Call SetIntro | g1manfre…dlf5 | ok |
| 00030B077A…CCA5FDBC | 271,243 | 17d ago | EnablePkg | — | ok |
| 72F71A363D…532552EF | 271,237 | 17d ago | AddPackage | — | ok |
Calls, deploys, and child packages in indexed history.