nativeify_test.gno
10.64 Kb · 272 lines
1package nativeify
2
3import (
4 "chain"
5 "chain/banker"
6 "strings"
7 "testing"
8
9 "gno.land/p/moul/x/envelope/v0"
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12 "gno.land/r/moul/x/grc20faucet/v0"
13 "gno.land/r/moul/x/nativereg/v0"
14)
15
16// Two things about the harness shape every test below.
17//
18// testing.SetRealm governs only the crossing calls made from the frame that
19// called it, so every account switch is inline in the test body.
20//
21// The BANK does not carry over between test functions while realm state does,
22// so native balances, TotalCoin and the escrowed GRC20 cannot be compared
23// across tests. Everything here is asserted as a delta inside one test.
24
25func TestNativeifyCreatesThePair(cur realm, t *testing.T) {
26 alice := testutils.TestAddress("nf-alice")
27 testing.SetRealm(testing.NewUserRealm(alice))
28
29 denom := Nativeify(cross(cur), grc20faucet.RedKey, "nred")
30 uassert.Equal(t, "/gno.land/r/moul/x/nativeify/v0:nred", denom)
31 uassert.Equal(t, denom, Denom("nred"))
32 uassert.Equal(t, grc20faucet.RedKey, TokenKey("nred"))
33
34 // The denom names this realm, so this realm is the only thing on the chain
35 // that can describe it, and it did.
36 e := nativereg.MustGet(denom)
37 uassert.Equal(t, Path, e.Issuer)
38 uassert.Equal(t, "nRED", e.Symbol)
39 uassert.Equal(t, 4, e.Decimals, "the underlying's decimals travel as a display hint")
40
41 uassert.AbortsContains(t, cur, "already issues nred", func() {
42 Nativeify(cross(cur), grc20faucet.BlueKey, "nred")
43 })
44 uassert.AbortsContains(t, cur, "already native here", func() {
45 Nativeify(cross(cur), grc20faucet.RedKey, "nred2")
46 })
47 uassert.AbortsContains(t, cur, "unknown token", func() {
48 Nativeify(cross(cur), "gno.land/r/nope/nope.NOPE", "nnope")
49 })
50}
51
52func TestBaseNameFollowsTheChainsRule(cur realm, t *testing.T) {
53 bob := testutils.TestAddress("nf-bob")
54 testing.SetRealm(testing.NewUserRealm(bob))
55
56 // The chain's own rule, applied here so the caller reads a sentence instead
57 // of a banker stack trace: a lowercase letter, then 2 to 15 more lowercase
58 // letters or digits.
59 uassert.AbortsContains(t, cur, "is 2 characters", func() {
60 Nativeify(cross(cur), grc20faucet.BlueKey, "ab")
61 })
62 uassert.AbortsContains(t, cur, "is 17 characters", func() {
63 Nativeify(cross(cur), grc20faucet.BlueKey, strings.Repeat("a", 17))
64 })
65 uassert.AbortsContains(t, cur, "is not", func() {
66 Nativeify(cross(cur), grc20faucet.BlueKey, "nBLUE")
67 })
68 uassert.AbortsContains(t, cur, "is not", func() {
69 Nativeify(cross(cur), grc20faucet.BlueKey, "1blue")
70 })
71 uassert.AbortsContains(t, cur, "is not", func() {
72 Nativeify(cross(cur), grc20faucet.BlueKey, "n-blue")
73 })
74
75 // And one that is legal all the way through.
76 uassert.Equal(t, "/gno.land/r/moul/x/nativeify/v0:nblue1",
77 Nativeify(cross(cur), grc20faucet.BlueKey, "nblue1"))
78}
79
80func TestWrapNeedsAnAllowanceFirst(cur realm, t *testing.T) {
81 carol := testutils.TestAddress("nf-carol")
82 testing.SetRealm(testing.NewUserRealm(carol))
83 grc20faucet.Claim(cross(cur))
84
85 // Until the holder names this realm on the underlying token, this realm has
86 // no authority over their balance whatsoever. That is the GRC20 half of the
87 // trade, and it is the half a native coin does not have.
88 uassert.AbortsContains(t, cur, "allowance", func() { Wrap(cross(cur), "nred", 1_000) })
89 uassert.Equal(t, int64(0), envelope.BalanceOf(carol, Denom("nred")))
90}
91
92func TestWrapEscrowsAndIssues(cur realm, t *testing.T) {
93 dave := testutils.TestAddress("nf-dave")
94 testing.SetRealm(testing.NewUserRealm(dave))
95 grc20faucet.Claim(cross(cur))
96 grc20faucet.Approve(cross(cur), "RED", Home(), 1_000_000)
97
98 denom := Denom("nred")
99 redBefore := grc20faucet.BalanceOf("RED", dave)
100 escrowBefore, issuedBefore := Solvency("nred")
101
102 got := Wrap(cross(cur), "nred", 400_000)
103
104 uassert.Equal(t, int64(400_000), got)
105 uassert.Equal(t, redBefore-400_000, grc20faucet.BalanceOf("RED", dave), "the GRC20 left the holder")
106 uassert.Equal(t, escrowBefore+400_000, grc20faucet.BalanceOf("RED", Home()), "and landed in escrow")
107 uassert.Equal(t, int64(400_000), envelope.BalanceOf(dave, denom), "the native coin is at the bank, 1:1")
108
109 escrowAfter, issuedAfter := Solvency("nred")
110 uassert.Equal(t, escrowBefore+400_000, escrowAfter)
111 uassert.Equal(t, issuedBefore+400_000, issuedAfter)
112 uassert.True(t, IsSolvent("nred"), "every coin issued is backed")
113
114 uassert.AbortsContains(t, cur, "positive amount", func() { Wrap(cross(cur), "nred", 0) })
115 uassert.AbortsContains(t, cur, "does not issue", func() { Wrap(cross(cur), "ghost", 1) })
116}
117
118func TestUnwrapDestroysTheCoinAndReleasesTheBacking(cur realm, t *testing.T) {
119 erin := testutils.TestAddress("nf-erin")
120 denom := Denom("nred")
121
122 testing.SetRealm(testing.NewUserRealm(erin))
123 grc20faucet.Claim(cross(cur))
124 grc20faucet.Approve(cross(cur), "RED", Home(), 1_000_000)
125 Wrap(cross(cur), "nred", 300_000)
126
127 // On chain the -send envelope moves the coins to this realm's address before
128 // Unwrap runs. The harness has no bank-send primitive, so the credit is
129 // issued here; SetOriginSend is the half the realm actually reads.
130 sent := chain.NewCoins(chain.NewCoin(denom, 120_000))
131 testing.IssueCoins(Home(), sent)
132 testing.SetOriginSend(sent)
133
134 redBefore := grc20faucet.BalanceOf("RED", erin)
135 escrowBefore := grc20faucet.BalanceOf("RED", Home())
136 _, issuedBefore := Solvency("nred")
137
138 got := Unwrap(cross(cur), "nred")
139
140 uassert.Equal(t, int64(120_000), got)
141 uassert.Equal(t, redBefore+120_000, grc20faucet.BalanceOf("RED", erin), "the backing came back")
142 uassert.Equal(t, escrowBefore-120_000, grc20faucet.BalanceOf("RED", Home()), "escrow shrank by the same")
143 _, issuedAfter := Solvency("nred")
144 uassert.Equal(t, issuedBefore-120_000, issuedAfter,
145 "the coins were destroyed, not moved: total supply fell")
146
147 testing.SetOriginSend(nil)
148 uassert.AbortsContains(t, cur, "must be paid", func() { Unwrap(cross(cur), "nred") })
149}
150
151func TestRender(cur realm, t *testing.T) {
152 index := Render("")
153 uassert.True(t, strings.Contains(index, "# nativeify"), index)
154 uassert.True(t, strings.Contains(index, Home().String()), "the escrow address is on the page")
155 uassert.True(t, strings.Contains(index, "| [nred](/r/moul/x/nativeify/v0:nred) |"),
156 "the pair is listed:\n"+index)
157
158 one := Render("nred")
159 uassert.True(t, strings.Contains(one, "/gno.land/r/moul/x/nativeify/v0:nred"), one)
160 uassert.True(t, strings.Contains(one, "a display hint only"), one)
161
162 uassert.True(t, strings.Contains(Render("ghost"), "404"), "an unknown base renders a 404")
163}
164
165// attemptPull is the thing a GRC20 facade over a native coin would have to do,
166// written out so it can be shown failing. It is in the test file, so it is not
167// part of the deployed package.
168func attemptPull(cur realm, from address, denom string, amount int64) {
169 banker.NewBanker(banker.BankerTypeRealmIssue, cur).
170 SendCoins(from, Home(), chain.NewCoins(chain.NewCoin(denom, amount)))
171}
172
173func TestARealmCannotPullANativeCoin(cur realm, t *testing.T) {
174 // The claim the package doc rests on, pinned rather than asserted: this
175 // realm holds the strongest banker the chain offers, the one that can mint
176 // and destroy this denom at will, and it still cannot MOVE a unit it does
177 // not already hold. There is no allowance to grant and no TransferFrom to
178 // call, which is why a grc20.Teller over a native coin cannot be written and
179 // why wugnot wraps in the direction it does.
180 frank := testutils.TestAddress("nf-frank")
181 denom := Denom("nred")
182 testing.IssueCoins(frank, chain.NewCoins(chain.NewCoin(denom, 1_000)))
183
184 msg := pullError(cur, frank, denom, 1_000)
185 uassert.True(t, strings.Contains(msg, "can only send coins from realm that created banker"),
186 "got: "+msg)
187 uassert.Equal(t, int64(1_000), envelope.BalanceOf(frank, denom), "and nothing moved")
188}
189
190// pullError runs attemptPull and returns the refusal as a string.
191//
192// uassert.PanicsContains cannot read this one. banker.SendCoins builds its
193// message as `"..." + b.pkgAddr + "..."`, and in gno a string concatenated with
194// an `address` has type `address`, so the panic value is an address rather than
195// a string and the helper reports "recover: unsupported type". Measured
196// 2026-09-28 against gno master; the message itself is correct, only its type is
197// surprising.
198func pullError(cur realm, from address, denom string, amount int64) (msg string) {
199 defer func() {
200 switch r := recover().(type) {
201 case nil:
202 msg = "it did not fail at all"
203 case address:
204 msg = string(r)
205 case string:
206 msg = r
207 case error:
208 msg = r.Error()
209 default:
210 msg = "panicked with an unexpected type"
211 }
212 }()
213 attemptPull(cur, from, denom, amount)
214 return ""
215}
216
217// ---------------------------------------------------------------------------
218// p/moul/x/envelope's Forward is exercised here rather than in its own package:
219// minting a BankerTypeOriginSend banker calls rlm.Previous(), and a p/ package's
220// test has no realm frame to walk, so it dies with "frame not found: cannot seek
221// beyond origin caller override". A p/ helper that needs a realm handle can only
222// be tested from a realm.
223
224func forwardDirect(cur realm, to address, coins chain.Coins) {
225 envelope.Forward(0, cur, to, coins)
226}
227
228func forwardOneDeeper(cur realm, to address, coins chain.Coins) {
229 forwardDirect(cur, to, coins)
230}
231
232func TestForwardIsBoundedByTheEnvelope(cur realm, t *testing.T) {
233 to := testutils.TestAddress("nf-forward-to")
234 denom := Denom("nred")
235
236 // The realm holds 5,000 of its own, and the envelope carried 250. A
237 // RealmSend banker could spend all 5,250; this one may spend 250.
238 testing.IssueCoins(Home(), chain.NewCoins(chain.NewCoin(denom, 5_000)))
239 sent := chain.NewCoins(chain.NewCoin(denom, 250))
240 testing.IssueCoins(Home(), sent)
241 testing.SetOriginSend(sent)
242
243 heldBefore := envelope.BalanceOf(Home(), denom)
244 forwardDirect(cur, to, sent)
245
246 uassert.Equal(t, int64(250), envelope.BalanceOf(to, denom))
247 uassert.Equal(t, heldBefore-250, envelope.BalanceOf(Home(), denom),
248 "exactly the envelope left, and none of the realm's own balance")
249
250 // Spending past the envelope is refused by the VM, not by any check here.
251 uassert.PanicsContains(t, cur, "limit", func() {
252 forwardDirect(cur, to, chain.NewCoins(chain.NewCoin(denom, 5_000)))
253 })
254 testing.SetOriginSend(nil)
255}
256
257func TestForwardFromOneFrameDeeper(cur realm, t *testing.T) {
258 // The frame-depth rule, measured rather than assumed: NewBanker refuses
259 // BankerTypeOriginSend unless rlm.Previous().IsUserCall(), and IsUserCall
260 // counts at most two call-boundary frames. Whatever this run reports is what
261 // the package doc says.
262 to := testutils.TestAddress("nf-deep-to")
263 denom := Denom("nred")
264 sent := chain.NewCoins(chain.NewCoin(denom, 90))
265 testing.IssueCoins(Home(), sent)
266 testing.SetOriginSend(sent)
267
268 forwardOneDeeper(cur, to, sent)
269 uassert.Equal(t, int64(90), envelope.BalanceOf(to, denom),
270 "one extra frame between the entry point and Forward")
271 testing.SetOriginSend(nil)
272}