1// Package envelope reads and forwards the coins a transaction attached to a2// call: the `-send` field of a `MsgCall`, credited to the called realm's address3// before a line of its code runs.4//5// It exists because "payable" is not a keyword in gno, it is a runtime fact. The6// VM rejects a `MsgCall` that carried coins the callee never looked at7// (gno.land/pkg/sdk/vm/keeper.go, `if !send.IsZero() && !*OriginSendObserved`),8// and reading the envelope is what counts as looking. So every realm that9// accepts payment writes the same three things by hand: read the amount, abort10// with something the caller can act on when it is zero or the wrong denom, and11// forward or keep it. This package is those three things.12//13// # Native coins are push-only, which is why this matters14//15// A realm cannot pull a native coin. `banker.SendCoins` panics unless the16// `from` address is the banker's own realm address, so there is no allowance17// and no `TransferFrom` for a native denom. The envelope is the entire inbound18// path: either the holder signs a bank send, or they attach coins to a call and19// the callee reads them here.20//21// That is also why a GRC20 needs `Approve` and a native coin does not. The GRC2022// grants standing authority to a spender; the envelope grants authority over one23// message and expires with it.24//25// # Who may forward, which is not about depth26//27// [Forward] mints a `BankerTypeOriginSend` banker, and `NewBanker` accepts that28// type only when `rlm.Previous().IsUserCall()` holds. `Realm.IsUserCall` is29// literally `pkgPath == ""` (gnovm/stdlibs/chain/runtime/frame.gno), so the test30// is about WHO entered the realm, not about how deep inside it you are:31//32// - `maketx call` straight into the realm: fine, and the realm may pass its33// `cur` down through as many of its own helpers as it likes. Measured at34// three nested calls, and through a function value and a closure.35// - realm A calls realm B, and B tries to forward: refused. B's previous is a36// code realm, so the envelope it was handed is not B's to route.37// - `maketx run`: refused. The run package is a code realm38// (`<domain>/e/<addr>/run`), so `IsUserCall` is false there too, and every39// payable function is therefore unreachable from a run script.40//41// The reading helpers have none of these conditions: they go through42// `unsafe.OriginSend()`, a context read that works anywhere, including inside a43// `Render`.44//45package envelope4647import (48 "chain"49 "chain/banker"50 "chain/runtime/unsafe"5152 "gno.land/p/nt/ufmt/v0"53)5455// Amount returns how much of denom this message's envelope carried, and zero56// when it carried none. Reading it is what makes the call payable.57func Amount(denom string) int64 {58 return unsafe.OriginSend().AmountOf(denom)59}6061// All returns the whole envelope, every denom in it.62//63// A call can be paid in several denoms at once, which is a property of the bank64// rather than of any realm: a `Coins` set holds up to 256 of them. A realm that65// only wants one should use [Only].66func All() chain.Coins {67 return unsafe.OriginSend()68}6970// IsEmpty reports whether nothing was attached to this call.71func IsEmpty() bool {72 return len(unsafe.OriginSend()) == 073}7475// Require returns [Amount] and aborts when it is zero, with a message naming the76// flag the caller left out.77//78// The message is the point. "insufficient funds" sends somebody to their wallet;79// naming the denom and the flag sends them to the fix.80func Require(denom string) int64 {81 amount := Amount(denom)82 if amount <= 0 {83 panic("this call must be paid: attach coins with -send <amount>" + denom)84 }85 return amount86}8788// RequireAtLeast is [Require] with a floor, and names both numbers when the89// envelope falls short.90func RequireAtLeast(denom string, min int64) int64 {91 amount := Require(denom)92 if amount < min {93 panic(ufmt.Sprintf("this call costs at least %d%s, the envelope carried %d",94 min, denom, amount))95 }96 return amount97}9899// RequireExactly is [Require] for a fixed price, and refuses an overpayment as100// well as an underpayment.101//102// Refusing to be overpaid is the unusual half, and it is deliberate: a realm103// that silently keeps the excess has invented a fee nobody agreed to, and a104// realm that refunds it has to move coins back out, which is a second failure105// mode. Aborting costs the caller one transaction and nothing else.106func RequireExactly(denom string, price int64) int64 {107 amount := Require(denom)108 if amount != price {109 panic(ufmt.Sprintf("this call costs exactly %d%s, the envelope carried %d",110 price, denom, amount))111 }112 return amount113}114115// Only is [Require] plus a refusal of anything else in the envelope.116//117// Use it wherever the realm has no code that would ever move a second denom,118// because coins it does not handle are coins stranded at its address forever:119// they are not refunded, and only the realm's own code can ever send them on.120func Only(denom string) int64 {121 amount := Require(denom)122 for _, coin := range unsafe.OriginSend() {123 if coin.Denom != denom {124 panic(ufmt.Sprintf("this call takes %s only, the envelope also carried %d%s",125 denom, coin.Amount, coin.Denom))126 }127 }128 return amount129}130131// Forward sends coins from the calling realm's address to `to`, bounded by this132// message's envelope.133//134// The banker it mints can spend the envelope and nothing else: not the realm's135// own balance, not a previous message's payment. That is what makes a routing136// function safe to write, and it is enforced by the VM rather than by the code137// here (`ctx.OriginSend.IsAllGTE(spent)`).138//139// It must be reached from a call a USER made into this realm; see the package140// doc for what that rules out.141//142// The leading `_ int` is not decoration: a non-realm package may not declare a143// crossing function, so a `p/` helper that needs a realm handle threads it as a144// later parameter. That is the same shape `p/nt/grc20`'s tellers use, and gno145// refuses the file outright without it ("crossing function (realm first146// argument) declared in non-realm package").147//148// This function has no test in this package either, for the same reason it has149// that signature: minting the banker calls `rlm.Previous()`, and a `p/` test has150// no realm frame to walk ("frame not found: cannot seek beyond origin caller151// override"). It is exercised from `r/moul/x/nativeify`'s tests instead.152func Forward(_ int, rlm realm, to address, coins chain.Coins) {153 banker.NewBanker(banker.BankerTypeOriginSend, rlm).SendCoins(rlm.Address(), to, coins)154}155156// ForwardAll forwards everything the envelope carried of denom, and returns it.157// It aborts when the envelope carried none, like [Require].158func ForwardAll(_ int, rlm realm, to address, denom string) int64 {159 amount := Require(denom)160 Forward(0, rlm, to, chain.NewCoins(chain.NewCoin(denom, amount)))161 return amount162}163164// BalanceOf returns what addr holds of denom right now, straight from the bank.165//166// It takes an address rather than a realm handle because it needs no authority167// at all: a readonly banker is constructible from anywhere, including a Render.168// Pass `cur.Address()` for the realm's own balance, envelope included.169func BalanceOf(addr address, denom string) int64 {170 return banker.NewReadonlyBanker().GetCoin(addr, denom)171}172Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.