1package coin23import (4 "strings"5 "testing"67 "gno.land/p/nt/seqid/v0"8 "gno.land/p/nt/testutils/v0"9 "gno.land/p/nt/uassert/v0"10 "gno.land/p/nt/urequire/v0"11)1213var fullPolicy = Policy{14 Mint: "1 per distinct replier",15 Sink: "burned to pin a thread",16 Buyer: "anyone who wants placement",17}1819// newTestCoin builds a Coin from a test. The same-realm cross promotes the20// test's EOA-origin cur into a code realm frame, which is what grc20.NewToken21// requires: it is the shape grc20's own tests use (vendor/.../token_test.gno).22func newTestCoin(t *testing.T, policy Policy, id seqid.ID, rlm realm) (c *Coin) {23 t.Helper()24 func(cur realm) {25 c = New("Test Points", "TEST", 0, id, policy, 0, cur)26 }(cross(rlm))27 return28}2930func TestPolicyValid(t *testing.T) {31 tests := []struct {32 name string33 p Policy34 want bool35 }{36 {"complete", fullPolicy, true},37 {"no mint rule", Policy{Sink: "s", Buyer: "b"}, false},38 {"no sink", Policy{Mint: "m", Buyer: "b"}, false},39 {"no buyer", Policy{Mint: "m", Sink: "s"}, false},40 {"empty", Policy{}, false},41 }42 for _, tt := range tests {43 uassert.Equal(t, tt.want, tt.p.Valid(), tt.name)44 }45}4647// New's refusal is asserted with AbortsContains and not PanicsContains: the48// constructor runs behind a same-realm cross (the frame grc20.NewToken49// requires), and a panic raised across a crossing call is an abort that no50// recover() in the caller can catch.51func TestNewRefusesAnIncompletePolicy(cur realm, t *testing.T) {52 uassert.AbortsContains(t, cur, "mint rule, a sink and a buyer", func() {53 newTestCoin(t, Policy{Mint: "m", Sink: "s"}, 0, cur)54 })55}5657func TestEarnAndSpendMoveSupplyInOppositeDirections(cur realm, t *testing.T) {58 c := newTestCoin(t, fullPolicy, 1, cur)59 alice := testutils.TestAddress("alice")6061 c.Earn(alice, 10)62 uassert.Equal(t, int64(10), c.BalanceOf(alice))63 uassert.Equal(t, int64(10), c.Supply())6465 c.Spend(alice, 4)66 uassert.Equal(t, int64(6), c.BalanceOf(alice))67 uassert.Equal(t, int64(6), c.Supply())6869 // The flows stay visible after the fact, which the supply alone cannot70 // show: 6 in supply could be 6 earned and nothing burned.71 uassert.Equal(t, int64(10), c.Earned())72 uassert.Equal(t, int64(4), c.Spent())73}7475func TestEarnOfNothingIsANoOp(cur realm, t *testing.T) {76 c := newTestCoin(t, fullPolicy, 2, cur)77 alice := testutils.TestAddress("alice")7879 c.Earn(alice, 0)80 c.Earn(alice, -5)81 uassert.Equal(t, int64(0), c.Supply())82 uassert.Equal(t, int64(0), c.Earned())83 uassert.Equal(t, 0, c.Holders())84}8586func TestSpendNamesBothNumbersWhenShort(cur realm, t *testing.T) {87 c := newTestCoin(t, fullPolicy, 3, cur)88 alice := testutils.TestAddress("alice")89 c.Earn(alice, 2)9091 uassert.PanicsContains(t, cur, "balance 2 is short of 5", func() {92 c.Spend(alice, 5)93 })94 uassert.PanicsContains(t, cur, "must be positive", func() {95 c.Spend(alice, 0)96 })97 uassert.Equal(t, int64(2), c.BalanceOf(alice))98}99100func TestRenderCarriesTheThreeDeclarations(cur realm, t *testing.T) {101 c := newTestCoin(t, fullPolicy, 4, cur)102 c.Earn(testutils.TestAddress("alice"), 7)103 c.Spend(testutils.TestAddress("alice"), 3)104105 out := c.Render()106 for _, want := range []string{107 "Test Points",108 "TEST",109 fullPolicy.Mint,110 fullPolicy.Sink,111 fullPolicy.Buyer,112 } {113 uassert.True(t, strings.Contains(out, want), "render is missing "+want)114 }115 // 4 in supply, 7 earned, 3 burned, 1 holder.116 uassert.True(t, strings.Contains(out, "| 4 | 7 | 3 | 1 |"), "render is missing the flow row: "+out)117}118119func TestTokenIsAnOrdinaryGRC20(cur realm, t *testing.T) {120 c := newTestCoin(t, fullPolicy, 5, cur)121 alice := testutils.TestAddress("alice")122 c.Earn(alice, 3)123124 tok := c.Token()125 urequire.False(t, tok == nil, "token should not be nil")126 uassert.Equal(t, "TEST", tok.GetSymbol())127 uassert.Equal(t, int64(3), tok.TotalSupply())128 uassert.Equal(t, int64(3), tok.BalanceOf(alice))129}130Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.