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

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}