Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package envelope reads and forwards the coins a transaction attached to a call: the \`-send\` field of a \`MsgCall\`,...

Readme View source

gno.land/p/moul/x/envelope/v0

Reading and forwarding the coins a transaction attached to a call. The -send field of a MsgCall, credited to the called realm's address before a line of its code runs.

"Payable" is not a keyword in gno, it is a runtime fact: the VM rejects a MsgCall that carried coins the callee never looked at, and reading the envelope is what counts as looking. So every realm taking payment writes the same three things by hand. This is those three things.

 1import "gno.land/p/moul/x/envelope/v0"
 2
 3func Publish(cur realm, slug string) {
 4    envelope.RequireExactly(Price, 500)   // aborts, naming the flag, if unpaid
 5    // ...
 6}
 7
 8func Route(cur realm, to address) int64 {
 9    return envelope.ForwardAll(0, cur, to, Denom)  // spends the envelope, nothing else
10}
Amount · All · IsEmpty read it, and make the call payable
Require · RequireAtLeast · RequireExactly read it and abort usefully when it is wrong
Only refuse an envelope carrying anything else
Forward · ForwardAll spend it, and nothing but it
BalanceOf what an address holds of a denom, from the bank

Why the abort messages are long

Require aborts with attach coins with -send <amount>/gno.land/r/...:coin. The abort is the realm's user interface at the moment somebody got it wrong: "insufficient funds" sends them to their wallet, naming the flag sends them to the fix.

RequireExactly refuses an overpayment too. A realm that silently keeps the excess has invented a fee nobody agreed to, and one that refunds it has to move coins back out, which is a second failure mode.

Two things that are not obvious

Forward is about who entered, not how deep you are. It mints a BankerTypeOriginSend banker, which NewBanker allows only when rlm.Previous().IsUserCall(), and that is literally pkgPath == "". So a realm may pass its cur down through as many of its own helpers as it likes (measured at three nested calls, through a function value, and through a closure), and it may not forward an envelope handed to it by another realm, nor be driven from gnokey maketx run, whose entry package is a code realm.

Forward is tested from a realm, not from here. Minting the banker calls rlm.Previous(), and a p/ package's test has no realm frame to walk: it dies with frame not found: cannot seek beyond origin caller override. The tests live in r/moul/x/nativeify, which is also the realm that uses it.

Related: a native coin is push-only, so the envelope is its entire inbound path. There is no allowance for one, which is what r/moul/x/nativeify is about.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/x/envelope/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package envelope reads and forwards the coins a transaction attached to a call: the `-send` field of a `MsgCall`, credited to the called realm's address before a line of its code runs.

It exists because "payable" is not a keyword in gno, it is a runtime fact. The VM rejects a `MsgCall` that carried coins the callee never looked at (gno.land/pkg/sdk/vm/keeper.go, `if !send.IsZero() && !*OriginSendObserved`), and reading the envelope is what counts as looking. So every realm that accepts payment writes the same three things by hand: read the amount, abort with something the caller can act on when it is zero or the wrong denom, and forward or keep it. This package is those three things.

Native coins are push-only, which is why this matters

A realm cannot pull a native coin. `banker.SendCoins` panics unless the `from` address is the banker's own realm address, so there is no allowance and no `TransferFrom` for a native denom. The envelope is the entire inbound path: either the holder signs a bank send, or they attach coins to a call and the callee reads them here.

That is also why a GRC20 needs `Approve` and a native coin does not. The GRC20 grants standing authority to a spender; the envelope grants authority over one message and expires with it.

Who may forward, which is not about depth

Forward mints a `BankerTypeOriginSend` banker, and `NewBanker` accepts that type only when `rlm.Previous().IsUserCall()` holds. `Realm.IsUserCall` is literally `pkgPath == ""` (gnovm/stdlibs/chain/runtime/frame.gno), so the test is about WHO entered the realm, not about how deep inside it you are:

  • `maketx call` straight into the realm: fine, and the realm may pass its `cur` down through as many of its own helpers as it likes. Measured at three nested calls, and through a function value and a closure.
  • realm A calls realm B, and B tries to forward: refused. B's previous is a code realm, so the envelope it was handed is not B's to route.
  • `maketx run`: refused. The run package is a code realm (`<domain>/e/<addr>/run`), so `IsUserCall` is false there too, and every payable function is therefore unreachable from a run script.

The reading helpers have none of these conditions: they go through `unsafe.OriginSend()`, a context read that works anywhere, including inside a `Render`.

Functions 10

func All

1func All() chain.Coins
source

All returns the whole envelope, every denom in it.

A call can be paid in several denoms at once, which is a property of the bank rather than of any realm: a `Coins` set holds up to 256 of them. A realm that only wants one should use Only.

func Amount

1func Amount(denom string) int64
source

Amount returns how much of denom this message's envelope carried, and zero when it carried none. Reading it is what makes the call payable.

func BalanceOf

1func BalanceOf(addr address, denom string) int64
source

BalanceOf returns what addr holds of denom right now, straight from the bank.

It takes an address rather than a realm handle because it needs no authority at all: a readonly banker is constructible from anywhere, including a Render. Pass `cur.Address()` for the realm's own balance, envelope included.

func Forward

1func Forward(_ int, rlm realm, to address, coins chain.Coins)
source

Forward sends coins from the calling realm's address to `to`, bounded by this message's envelope.

The banker it mints can spend the envelope and nothing else: not the realm's own balance, not a previous message's payment. That is what makes a routing function safe to write, and it is enforced by the VM rather than by the code here (`ctx.OriginSend.IsAllGTE(spent)`).

It must be reached from a call a USER made into this realm; see the package doc for what that rules out.

The leading `_ int` is not decoration: a non-realm package may not declare a crossing function, so a `p/` helper that needs a realm handle threads it as a later parameter. That is the same shape `p/nt/grc20`'s tellers use, and gno refuses the file outright without it ("crossing function (realm first argument) declared in non-realm package").

This function has no test in this package either, for the same reason it has that signature: minting the banker calls `rlm.Previous()`, and a `p/` test has no realm frame to walk ("frame not found: cannot seek beyond origin caller override"). It is exercised from `r/moul/x/nativeify`'s tests instead.

func ForwardAll

1func ForwardAll(_ int, rlm realm, to address, denom string) int64
source

ForwardAll forwards everything the envelope carried of denom, and returns it. It aborts when the envelope carried none, like Require.

func IsEmpty

1func IsEmpty() bool
source

IsEmpty reports whether nothing was attached to this call.

func Only

1func Only(denom string) int64
source

Only is Require plus a refusal of anything else in the envelope.

Use it wherever the realm has no code that would ever move a second denom, because coins it does not handle are coins stranded at its address forever: they are not refunded, and only the realm's own code can ever send them on.

func Require

1func Require(denom string) int64
source

Require returns Amount and aborts when it is zero, with a message naming the flag the caller left out.

The message is the point. "insufficient funds" sends somebody to their wallet; naming the denom and the flag sends them to the fix.

func RequireAtLeast

1func RequireAtLeast(denom string, min int64) int64
source

RequireAtLeast is Require with a floor, and names both numbers when the envelope falls short.

func RequireExactly

1func RequireExactly(denom string, price int64) int64
source

RequireExactly is Require for a fixed price, and refuses an overpayment as well as an underpayment.

Refusing to be overpaid is the unusual half, and it is deliberate: a realm that silently keeps the excess has invented a fee nobody agreed to, and a realm that refunds it has to move coins back out, which is a second failure mode. Aborting costs the caller one transaction and nothing else.

Imports 4

Source Files 4