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

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}