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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

envelope.gno

7.12 Kb · 171 lines
  1// Package envelope reads and forwards the coins a transaction attached to a
  2// call: the `-send` field of a `MsgCall`, credited to the called realm's address
  3// before a line of its code runs.
  4//
  5// It exists because "payable" is not a keyword in gno, it is a runtime fact. The
  6// VM rejects a `MsgCall` that carried coins the callee never looked at
  7// (gno.land/pkg/sdk/vm/keeper.go, `if !send.IsZero() && !*OriginSendObserved`),
  8// and reading the envelope is what counts as looking. So every realm that
  9// accepts payment writes the same three things by hand: read the amount, abort
 10// with something the caller can act on when it is zero or the wrong denom, and
 11// forward or keep it. This package is those three things.
 12//
 13// # Native coins are push-only, which is why this matters
 14//
 15// A realm cannot pull a native coin. `banker.SendCoins` panics unless the
 16// `from` address is the banker's own realm address, so there is no allowance
 17// and no `TransferFrom` for a native denom. The envelope is the entire inbound
 18// path: either the holder signs a bank send, or they attach coins to a call and
 19// the callee reads them here.
 20//
 21// That is also why a GRC20 needs `Approve` and a native coin does not. The GRC20
 22// grants standing authority to a spender; the envelope grants authority over one
 23// message and expires with it.
 24//
 25// # Who may forward, which is not about depth
 26//
 27// [Forward] mints a `BankerTypeOriginSend` banker, and `NewBanker` accepts that
 28// type only when `rlm.Previous().IsUserCall()` holds. `Realm.IsUserCall` is
 29// literally `pkgPath == ""` (gnovm/stdlibs/chain/runtime/frame.gno), so the test
 30// 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 its
 33//     `cur` down through as many of its own helpers as it likes. Measured at
 34//     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 a
 36//     code realm, so the envelope it was handed is not B's to route.
 37//   - `maketx run`: refused. The run package is a code realm
 38//     (`<domain>/e/<addr>/run`), so `IsUserCall` is false there too, and every
 39//     payable function is therefore unreachable from a run script.
 40//
 41// The reading helpers have none of these conditions: they go through
 42// `unsafe.OriginSend()`, a context read that works anywhere, including inside a
 43// `Render`.
 44//
 45package envelope
 46
 47import (
 48	"chain"
 49	"chain/banker"
 50	"chain/runtime/unsafe"
 51
 52	"gno.land/p/nt/ufmt/v0"
 53)
 54
 55// Amount returns how much of denom this message's envelope carried, and zero
 56// when it carried none. Reading it is what makes the call payable.
 57func Amount(denom string) int64 {
 58	return unsafe.OriginSend().AmountOf(denom)
 59}
 60
 61// 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 bank
 64// rather than of any realm: a `Coins` set holds up to 256 of them. A realm that
 65// only wants one should use [Only].
 66func All() chain.Coins {
 67	return unsafe.OriginSend()
 68}
 69
 70// IsEmpty reports whether nothing was attached to this call.
 71func IsEmpty() bool {
 72	return len(unsafe.OriginSend()) == 0
 73}
 74
 75// Require returns [Amount] and aborts when it is zero, with a message naming the
 76// 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 amount
 86}
 87
 88// RequireAtLeast is [Require] with a floor, and names both numbers when the
 89// 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 amount
 97}
 98
 99// RequireExactly is [Require] for a fixed price, and refuses an overpayment as
100// well as an underpayment.
101//
102// Refusing to be overpaid is the unusual half, and it is deliberate: a realm
103// that silently keeps the excess has invented a fee nobody agreed to, and a
104// realm that refunds it has to move coins back out, which is a second failure
105// 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 amount
113}
114
115// 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 amount
129}
130
131// Forward sends coins from the calling realm's address to `to`, bounded by this
132// message's envelope.
133//
134// The banker it mints can spend the envelope and nothing else: not the realm's
135// own balance, not a previous message's payment. That is what makes a routing
136// function safe to write, and it is enforced by the VM rather than by the code
137// here (`ctx.OriginSend.IsAllGTE(spent)`).
138//
139// It must be reached from a call a USER made into this realm; see the package
140// doc for what that rules out.
141//
142// The leading `_ int` is not decoration: a non-realm package may not declare a
143// crossing function, so a `p/` helper that needs a realm handle threads it as a
144// later parameter. That is the same shape `p/nt/grc20`'s tellers use, and gno
145// refuses the file outright without it ("crossing function (realm first
146// argument) declared in non-realm package").
147//
148// This function has no test in this package either, for the same reason it has
149// that signature: minting the banker calls `rlm.Previous()`, and a `p/` test has
150// no realm frame to walk ("frame not found: cannot seek beyond origin caller
151// 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}
155
156// 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 amount
162}
163
164// 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 authority
167// 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}