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}