PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/nt/grc721/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
nt / grc721
Files
8 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/nt/grc721/v0
gno
0.9

Files (8)

  • gnomod.tomltoml
  • tellers.gnogno
  • token.gnogno
  • types.gnogno
  • newtoken_event_filetest.gnogno
  • tellers_test.gnogno
  • token_test.gnogno
  • veto_no_event_filetest.gnogno
  • token_test.gnogno
    1package grc72123import (4	"strings"5	"testing"67	"gno.land/p/nt/seqid/v0"8

    Functions

    not supported for pure packages by the node (vm/qfuncs)

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    "gno.land/p/nt/testutils/v0"
    9 "gno.land/p/nt/uassert/v0"
    10 "gno.land/p/nt/urequire/v0"
    11)
    12
    13func mustBalanceOf(t *testing.T, tok *Token, addr address) int64 {
    14 balance, err := tok.BalanceOf(addr)
    15 urequire.NoError(t, err)
    16 return balance
    17}
    18
    19// newTestToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).
    20func newTestToken(name, symbol string, id seqid.ID, rlm realm) (tok *Token, led *PrivateLedger) {
    21 func(cur realm) {
    22 tok, led = NewToken(name, symbol, id, cur)
    23 }(cross(rlm))
    24 return
    25}
    26
    27type mockExtension struct {
    28 mints, transfers, burns int
    29 lastMintTo address
    30 lastTransferFrom address
    31 lastTransferTo address
    32 lastBurn TokenID
    33}
    34
    35func (m *mockExtension) ExtensionKind() string { return "mock" }
    36
    37func (m *mockExtension) OnMint(to address, tid TokenID) {
    38 m.mints++
    39 m.lastMintTo = to
    40}
    41
    42func (m *mockExtension) OnTransfer(from, to address, tid TokenID) {
    43 m.transfers++
    44 m.lastTransferFrom = from
    45 m.lastTransferTo = to
    46}
    47
    48func (m *mockExtension) OnBurn(tid TokenID) {
    49 m.burns++
    50 m.lastBurn = tid
    51}
    52
    53// Value type holding a slice: uncomparable, so `==` on two of them is a runtime fault.
    54type journalExtension struct{ seen []TokenID }
    55
    56func (e journalExtension) ExtensionKind() string { return "journal" }
    57func (e journalExtension) OnMint(to address, tid TokenID) {}
    58func (e journalExtension) OnTransfer(from, to address, tid TokenID) {}
    59func (e journalExtension) OnBurn(tid TokenID) {}
    60
    61var (
    62 alice = testutils.TestAddress("alice")
    63 bob = testutils.TestAddress("bob")
    64 carl = testutils.TestAddress("carl")
    65)
    66
    67func TestNewToken(cur realm, t *testing.T) {
    68 tok, led := newTestToken("Foo NFT", "FOO", 0, cur)
    69 urequire.False(t, tok == nil, "token should not be nil")
    70 urequire.False(t, led == nil, "ledger should not be nil")
    71
    72 uassert.Equal(t, "Foo NFT", tok.GetName())
    73 uassert.Equal(t, "FOO", tok.GetSymbol())
    74 uassert.Equal(t, int64(0), tok.TotalSupply())
    75 uassert.Equal(t, 0, tok.KnownAccounts())
    76 uassert.True(t, strings.HasSuffix(tok.ID(), ".FOO."+seqid.ID(0).String()), "ID must end with .symbol.id")
    77 uassert.True(t, strings.HasPrefix(tok.ID(), "gno.land/"), "ID must begin with the origin realm path")
    78 uassert.Equal(t, tok.ID(), led.ReadToken().ID())
    79}
    80
    81func TestNewTokenDistinctIDs(cur realm, t *testing.T) {
    82 first, _ := newTestToken("Same", "DUP", 1, cur)
    83 second, _ := newTestToken("Same", "DUP", 2, cur)
    84 uassert.True(t, first.ID() != second.ID(), "distinct seqid must yield distinct IDs")
    85
    86 a, _ := newTestToken("Same", "DUP", 3, cur)
    87 b, _ := newTestToken("Same", "DUP", 3, cur)
    88 uassert.Equal(t, a.ID(), b.ID())
    89}
    90
    91func TestNewTokenValidation(cur realm, t *testing.T) {
    92 longName := strings.Repeat("a", MaxNameLen+1)
    93 maxName := strings.Repeat("a", MaxNameLen)
    94 longSymbol := strings.Repeat("A", MaxSymbolLen+1)
    95
    96 tests := []struct {
    97 name string
    98 tokenName string
    99 symbol string
    100 wantAbort bool
    101 wantSubstr string
    102 }{
    103 {"valid name and symbol succeeds", "Foo", "FOO", false, ""},
    104 {"symbol with full slug charset succeeds", "Foo", "aZ9_-", false, ""},
    105 {"name at max length succeeds", maxName, "FOO", false, ""},
    106 {"empty name returns ErrInvalidName", "", "FOO", true, "invalid token name"},
    107 {"name over max length returns ErrInvalidName", longName, "FOO", true, "invalid token name"},
    108 {"name with low control char returns ErrInvalidName", "Foo\x01", "FOO", true, "invalid token name"},
    109 {"name with DEL control char returns ErrInvalidName", "Foo\x7f", "FOO", true, "invalid token name"},
    110 {"empty symbol returns ErrInvalidSymbol", "Foo", "", true, "invalid token symbol"},
    111 {"symbol over max length returns ErrInvalidSymbol", "Foo", longSymbol, true, "invalid token symbol"},
    112 {"symbol with space returns ErrInvalidSymbol", "Foo", "has space", true, "invalid token symbol"},
    113 {"symbol with dot returns ErrInvalidSymbol", "Foo", "FO.O", true, "invalid token symbol"},
    114 {"symbol with slash returns ErrInvalidSymbol", "Foo", "FO/O", true, "invalid token symbol"},
    115 }
    116
    117 for _, tc := range tests {
    118 tc := tc
    119 t.Run(tc.name, func(t *testing.T) {
    120 if tc.wantAbort {
    121 urequire.AbortsContains(t, cur, tc.wantSubstr, func() {
    122 newTestToken(tc.tokenName, tc.symbol, 0, cur)
    123 })
    124 return
    125 }
    126 tok, _ := newTestToken(tc.tokenName, tc.symbol, 0, cur)
    127 uassert.Equal(t, tc.tokenName, tok.GetName())
    128 uassert.Equal(t, tc.symbol, tok.GetSymbol())
    129 })
    130 }
    131}
    132
    133func TestNewTokenRejectsSpoofedRealm(cur realm, t *testing.T) {
    134 // Stale outer cur in a fresh frame → ErrSpoofedRealm.
    135 urequire.AbortsContains(t, cur, "rlm does not match the current crossing frame", func() {
    136 stale := cur
    137 func(cur realm) {
    138 NewToken("Foo", "FOO", 0, stale)
    139 }(cross(cur))
    140 })
    141}
    142
    143func TestMint(cur realm, t *testing.T) {
    144 tests := []struct {
    145 name string
    146 action func(led *PrivateLedger) error
    147 wantErr error
    148 verify func(t *testing.T, tok *Token, led *PrivateLedger)
    149 }{
    150 {
    151 name: "mint to valid address succeeds",
    152 action: func(led *PrivateLedger) error {
    153 return led.Mint(alice, "1")
    154 },
    155 wantErr: nil,
    156 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    157 uassert.Equal(t, int64(1), tok.TotalSupply())
    158 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))
    159 uassert.Equal(t, 1, tok.KnownAccounts())
    160 owner, err := tok.OwnerOf("1")
    161 urequire.NoError(t, err)
    162 uassert.Equal(t, alice, owner)
    163 },
    164 },
    165 {
    166 name: "mint to invalid address returns ErrInvalidAddress",
    167 action: func(led *PrivateLedger) error {
    168 return led.Mint(zeroAddress, "1")
    169 },
    170 wantErr: ErrInvalidAddress,
    171 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    172 uassert.Equal(t, int64(0), tok.TotalSupply())
    173 },
    174 },
    175 {
    176 name: "mint duplicate token id returns ErrTokenIdAlreadyExists",
    177 action: func(led *PrivateLedger) error {
    178 urequire.NoError(t, led.Mint(alice, "1"))
    179 return led.Mint(bob, "1")
    180 },
    181 wantErr: ErrTokenIdAlreadyExists,
    182 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    183 owner, err := tok.OwnerOf("1")
    184 urequire.NoError(t, err)
    185 uassert.Equal(t, alice, owner)
    186 uassert.Equal(t, int64(1), tok.TotalSupply())
    187 },
    188 },
    189 {
    190 name: "minting distinct ids to same owner accumulates balance",
    191 action: func(led *PrivateLedger) error {
    192 urequire.NoError(t, led.Mint(alice, "1"))
    193 return led.Mint(alice, "2")
    194 },
    195 wantErr: nil,
    196 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    197 uassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))
    198 uassert.Equal(t, int64(2), tok.TotalSupply())
    199 uassert.Equal(t, 1, tok.KnownAccounts())
    200 },
    201 },
    202 }
    203
    204 for _, tc := range tests {
    205 tc := tc
    206 t.Run(tc.name, func(t *testing.T) {
    207 tok, led := newTestToken("Foo", "FOO", 0, cur)
    208 err := tc.action(led)
    209 if tc.wantErr == nil {
    210 uassert.NoError(t, err)
    211 } else {
    212 uassert.ErrorIs(t, err, tc.wantErr)
    213 }
    214 if tc.verify != nil {
    215 tc.verify(t, tok, led)
    216 }
    217 })
    218 }
    219}
    220
    221func TestBurn(cur realm, t *testing.T) {
    222 tests := []struct {
    223 name string
    224 action func(led *PrivateLedger) error
    225 wantErr error
    226 verify func(t *testing.T, tok *Token, led *PrivateLedger)
    227 }{
    228 {
    229 name: "burn existing token clears all state",
    230 action: func(led *PrivateLedger) error {
    231 urequire.NoError(t, led.Mint(alice, "1"))
    232 return led.Burn("1")
    233 },
    234 wantErr: nil,
    235 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    236 uassert.Equal(t, int64(0), tok.TotalSupply())
    237 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))
    238 uassert.Equal(t, 0, tok.KnownAccounts())
    239 _, err := tok.OwnerOf("1")
    240 uassert.ErrorIs(t, err, ErrInvalidTokenId)
    241 },
    242 },
    243 {
    244 name: "burn clears any outstanding approval",
    245 action: func(led *PrivateLedger) error {
    246 urequire.NoError(t, led.Mint(alice, "1"))
    247 urequire.NoError(t, led.Approve(alice, bob, "1"))
    248 return led.Burn("1")
    249 },
    250 wantErr: nil,
    251 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    252 uassert.Equal(t, 0, led.tokenApprovals.Size())
    253 _, err := tok.GetApproved("1")
    254 uassert.ErrorIs(t, err, ErrInvalidTokenId)
    255 },
    256 },
    257 {
    258 name: "burn missing token returns ErrInvalidTokenId",
    259 action: func(led *PrivateLedger) error {
    260 return led.Burn("999")
    261 },
    262 wantErr: ErrInvalidTokenId,
    263 },
    264 {
    265 name: "burn one of two tokens keeps the account known",
    266 action: func(led *PrivateLedger) error {
    267 urequire.NoError(t, led.Mint(alice, "1"))
    268 urequire.NoError(t, led.Mint(alice, "2"))
    269 return led.Burn("1")
    270 },
    271 wantErr: nil,
    272 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    273 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))
    274 uassert.Equal(t, 1, tok.KnownAccounts())
    275 uassert.Equal(t, int64(1), tok.TotalSupply())
    276 },
    277 },
    278 }
    279
    280 for _, tc := range tests {
    281 tc := tc
    282 t.Run(tc.name, func(t *testing.T) {
    283 tok, led := newTestToken("Foo", "FOO", 0, cur)
    284 err := tc.action(led)
    285 if tc.wantErr == nil {
    286 uassert.NoError(t, err)
    287 } else {
    288 uassert.ErrorIs(t, err, tc.wantErr)
    289 }
    290 if tc.verify != nil {
    291 tc.verify(t, tok, led)
    292 }
    293 })
    294 }
    295}
    296
    297func TestApprove(cur realm, t *testing.T) {
    298 tests := []struct {
    299 name string
    300 action func(led *PrivateLedger) error
    301 wantErr error
    302 verify func(t *testing.T, tok *Token, led *PrivateLedger)
    303 }{
    304 {
    305 name: "owner approves another account",
    306 action: func(led *PrivateLedger) error {
    307 urequire.NoError(t, led.Mint(alice, "1"))
    308 return led.Approve(alice, bob, "1")
    309 },
    310 wantErr: nil,
    311 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    312 approved, err := tok.GetApproved("1")
    313 urequire.NoError(t, err)
    314 uassert.Equal(t, bob, approved)
    315 },
    316 },
    317 {
    318 name: "operator approves on behalf of owner",
    319 action: func(led *PrivateLedger) error {
    320 urequire.NoError(t, led.Mint(alice, "1"))
    321 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
    322 return led.Approve(bob, carl, "1")
    323 },
    324 wantErr: nil,
    325 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    326 approved, err := tok.GetApproved("1")
    327 urequire.NoError(t, err)
    328 uassert.Equal(t, carl, approved)
    329 },
    330 },
    331 {
    332 name: "approve to zero address revokes an existing approval (EIP-721)",
    333 action: func(led *PrivateLedger) error {
    334 urequire.NoError(t, led.Mint(alice, "1"))
    335 urequire.NoError(t, led.Approve(alice, bob, "1"))
    336 return led.Approve(alice, zeroAddress, "1")
    337 },
    338 wantErr: nil,
    339 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    340 _, err := tok.GetApproved("1")
    341 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
    342 },
    343 },
    344 {
    345 name: "revoke with no prior approval is a no-op",
    346 action: func(led *PrivateLedger) error {
    347 urequire.NoError(t, led.Mint(alice, "1"))
    348 return led.Approve(alice, zeroAddress, "1")
    349 },
    350 wantErr: nil,
    351 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    352 _, err := tok.GetApproved("1")
    353 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
    354 },
    355 },
    356 {
    357 name: "approve on missing token returns ErrInvalidTokenId",
    358 action: func(led *PrivateLedger) error {
    359 return led.Approve(alice, bob, "999")
    360 },
    361 wantErr: ErrInvalidTokenId,
    362 },
    363 {
    364 name: "approve to current owner returns ErrApprovalToCurrentOwner",
    365 action: func(led *PrivateLedger) error {
    366 urequire.NoError(t, led.Mint(alice, "1"))
    367 return led.Approve(alice, alice, "1")
    368 },
    369 wantErr: ErrApprovalToCurrentOwner,
    370 },
    371 {
    372 name: "non-owner non-operator approve returns ErrCallerIsNotOwnerOrApproved",
    373 action: func(led *PrivateLedger) error {
    374 urequire.NoError(t, led.Mint(alice, "1"))
    375 return led.Approve(bob, carl, "1")
    376 },
    377 wantErr: ErrCallerIsNotOwnerOrApproved,
    378 },
    379 }
    380
    381 for _, tc := range tests {
    382 tc := tc
    383 t.Run(tc.name, func(t *testing.T) {
    384 tok, led := newTestToken("Foo", "FOO", 0, cur)
    385 err := tc.action(led)
    386 if tc.wantErr == nil {
    387 uassert.NoError(t, err)
    388 } else {
    389 uassert.ErrorIs(t, err, tc.wantErr)
    390 }
    391 if tc.verify != nil {
    392 tc.verify(t, tok, led)
    393 }
    394 })
    395 }
    396}
    397
    398func TestSetApprovalForAll(cur realm, t *testing.T) {
    399 tests := []struct {
    400 name string
    401 action func(led *PrivateLedger) error
    402 wantErr error
    403 verify func(t *testing.T, tok *Token, led *PrivateLedger)
    404 }{
    405 {
    406 name: "grant operator approval",
    407 action: func(led *PrivateLedger) error {
    408 return led.SetApprovalForAll(alice, bob, true)
    409 },
    410 wantErr: nil,
    411 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    412 uassert.True(t, tok.IsApprovedForAll(alice, bob))
    413 },
    414 },
    415 {
    416 name: "revoke operator approval releases the node",
    417 action: func(led *PrivateLedger) error {
    418 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
    419 return led.SetApprovalForAll(alice, bob, false)
    420 },
    421 wantErr: nil,
    422 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    423 uassert.False(t, tok.IsApprovedForAll(alice, bob))
    424 uassert.Equal(t, 0, led.operatorApprovals.Size())
    425 },
    426 },
    427 {
    428 name: "revoke without a prior grant stores nothing",
    429 action: func(led *PrivateLedger) error {
    430 return led.SetApprovalForAll(alice, bob, false)
    431 },
    432 wantErr: nil,
    433 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    434 uassert.False(t, tok.IsApprovedForAll(alice, bob))
    435 uassert.Equal(t, 0, led.operatorApprovals.Size())
    436 },
    437 },
    438 {
    439 name: "invalid operator returns ErrInvalidAddress",
    440 action: func(led *PrivateLedger) error {
    441 return led.SetApprovalForAll(alice, zeroAddress, true)
    442 },
    443 wantErr: ErrInvalidAddress,
    444 },
    445 {
    446 name: "owner equals operator returns ErrApprovalToCurrentOwner",
    447 action: func(led *PrivateLedger) error {
    448 return led.SetApprovalForAll(alice, alice, true)
    449 },
    450 wantErr: ErrApprovalToCurrentOwner,
    451 },
    452 }
    453
    454 for _, tc := range tests {
    455 tc := tc
    456 t.Run(tc.name, func(t *testing.T) {
    457 tok, led := newTestToken("Foo", "FOO", 0, cur)
    458 err := tc.action(led)
    459 if tc.wantErr == nil {
    460 uassert.NoError(t, err)
    461 } else {
    462 uassert.ErrorIs(t, err, tc.wantErr)
    463 }
    464 if tc.verify != nil {
    465 tc.verify(t, tok, led)
    466 }
    467 })
    468 }
    469}
    470
    471func TestTransferFrom(cur realm, t *testing.T) {
    472 tests := []struct {
    473 name string
    474 action func(led *PrivateLedger) error
    475 wantErr error
    476 verify func(t *testing.T, tok *Token, led *PrivateLedger)
    477 }{
    478 {
    479 name: "owner transfers own token",
    480 action: func(led *PrivateLedger) error {
    481 urequire.NoError(t, led.Mint(alice, "1"))
    482 return led.TransferFrom(alice, alice, bob, "1")
    483 },
    484 wantErr: nil,
    485 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    486 owner, err := tok.OwnerOf("1")
    487 urequire.NoError(t, err)
    488 uassert.Equal(t, bob, owner)
    489 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))
    490 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, bob))
    491 },
    492 },
    493 {
    494 name: "approved spender transfers and approval is cleared",
    495 action: func(led *PrivateLedger) error {
    496 urequire.NoError(t, led.Mint(alice, "1"))
    497 urequire.NoError(t, led.Approve(alice, bob, "1"))
    498 return led.TransferFrom(bob, alice, carl, "1")
    499 },
    500 wantErr: nil,
    501 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    502 owner, err := tok.OwnerOf("1")
    503 urequire.NoError(t, err)
    504 uassert.Equal(t, carl, owner)
    505 _, err = tok.GetApproved("1")
    506 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
    507 },
    508 },
    509 {
    510 name: "operator transfers owner token",
    511 action: func(led *PrivateLedger) error {
    512 urequire.NoError(t, led.Mint(alice, "1"))
    513 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
    514 return led.TransferFrom(bob, alice, carl, "1")
    515 },
    516 wantErr: nil,
    517 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
    518 owner, err := tok.OwnerOf("1")
    519 urequire.NoError(t, err)
    520 uassert.Equal(t, carl, owner)
    521 },
    522 },
    523 {
    524 name: "unauthorized spender returns ErrCallerIsNotOwnerOrApproved",
    525 action: func(led *PrivateLedger) error {
    526 urequire.NoError(t, led.Mint(alice, "1"))
    527 return led.TransferFrom(bob, alice, carl, "1")
    528 },
    529 wantErr: ErrCallerIsNotOwnerOrApproved,
    530 },
    531 {
    532 name: "spender on missing token returns ErrCallerIsNotOwnerOrApproved",
    533 action: func(led *PrivateLedger) error {
    534 return led.TransferFrom(alice, alice, bob, "999")
    535 },
    536 wantErr: ErrCallerIsNotOwnerOrApproved,
    537 },
    538 {
    539 name: "transfer to self returns ErrCannotTransferToSelf",
    540 action: func(led *PrivateLedger) error {
    541 urequire.NoError(t, led.Mint(alice, "1"))
    542 return led.TransferFrom(alice, alice, alice, "1")
    543 },
    544 wantErr: ErrCannotTransferToSelf,
    545 },
    546 {
    547 name: "transfer from invalid address returns ErrInvalidAddress",
    548 action: func(led *PrivateLedger) error {
    549 urequire.NoError(t, led.Mint(alice, "1"))
    550 return led.TransferFrom(alice, zeroAddress, bob, "1")
    551 },
    552 wantErr: ErrInvalidAddress,
    553 },
    554 {
    555 name: "transfer to invalid address returns ErrInvalidAddress",
    556 action: func(led *PrivateLedger) error {
    557 urequire.NoError(t, led.Mint(alice, "1"))
    558 return led.TransferFrom(alice, alice, zeroAddress, "1")
    559 },
    560 wantErr: ErrInvalidAddress,
    561 },
    562 {
    563 name: "transfer from incorrect owner returns ErrTransferFromIncorrectOwner",
    564 action: func(led *PrivateLedger) error {
    565 urequire.NoError(t, led.Mint(alice, "1"))
    566 return led.TransferFrom(alice, bob, carl, "1")
    567 },
    568 wantErr: ErrTransferFromIncorrectOwner,
    569 },
    570 }
    571
    572 for _, tc := range tests {
    573 tc := tc
    574 t.Run(tc.name, func(t *testing.T) {
    575 tok, led := newTestToken("Foo", "FOO", 0, cur)
    576 err := tc.action(led)
    577 if tc.wantErr == nil {
    578 uassert.NoError(t, err)
    579 } else {
    580 uassert.ErrorIs(t, err, tc.wantErr)
    581 }
    582 if tc.verify != nil {
    583 tc.verify(t, tok, led)
    584 }
    585 })
    586 }
    587}
    588
    589func TestReadViews(cur realm, t *testing.T) {
    590 tok, led := newTestToken("Foo NFT", "FOO", 0, cur)
    591 urequire.NoError(t, led.Mint(alice, "1"))
    592 urequire.NoError(t, led.Mint(alice, "2"))
    593 urequire.NoError(t, led.Mint(bob, "3"))
    594 urequire.NoError(t, led.Approve(alice, carl, "1"))
    595 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
    596
    597 tests := []struct {
    598 name string
    599 check func(t *testing.T)
    600 }{
    601 {"GetName returns collection name", func(t *testing.T) {
    602 uassert.Equal(t, "Foo NFT", tok.GetName())
    603 }},
    604 {"GetSymbol returns collection symbol", func(t *testing.T) {
    605 uassert.Equal(t, "FOO", tok.GetSymbol())
    606 }},
    607 {"ID carries realm, symbol and seqid", func(t *testing.T) {
    608 uassert.True(t, strings.HasPrefix(tok.ID(), "gno.land/"))
    609 uassert.True(t, strings.Contains(tok.ID(), ".FOO."))
    610 }},
    611 {"TotalSupply counts live tokens", func(t *testing.T) {
    612 uassert.Equal(t, int64(3), tok.TotalSupply())
    613 }},
    614 {"KnownAccounts counts distinct holders", func(t *testing.T) {
    615 uassert.Equal(t, 2, tok.KnownAccounts())
    616 }},
    617 {"BalanceOf of a holder", func(t *testing.T) {
    618 uassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))
    619 }},
    620 {"BalanceOf of a non-holder is zero", func(t *testing.T) {
    621 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, carl))
    622 }},
    623 {"BalanceOf of the zero address returns ErrInvalidAddress", func(t *testing.T) {
    624 _, err := tok.BalanceOf(zeroAddress)
    625 uassert.ErrorIs(t, err, ErrInvalidAddress)
    626 }},
    627 {"OwnerOf existing token", func(t *testing.T) {
    628 owner, err := tok.OwnerOf("3")
    629 urequire.NoError(t, err)
    630 uassert.Equal(t, bob, owner)
    631 }},
    632 {"OwnerOf missing token returns ErrInvalidTokenId", func(t *testing.T) {
    633 _, err := tok.OwnerOf("999")
    634 uassert.ErrorIs(t, err, ErrInvalidTokenId)
    635 }},
    636 {"GetApproved returns the approved account", func(t *testing.T) {
    637 approved, err := tok.GetApproved("1")
    638 urequire.NoError(t, err)
    639 uassert.Equal(t, carl, approved)
    640 }},
    641 {"GetApproved on unapproved token returns ErrTokenIdNotApproved", func(t *testing.T) {
    642 _, err := tok.GetApproved("2")
    643 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
    644 }},
    645 {"GetApproved on missing token returns ErrInvalidTokenId", func(t *testing.T) {
    646 _, err := tok.GetApproved("999")
    647 uassert.ErrorIs(t, err, ErrInvalidTokenId)
    648 }},
    649 {"IsApprovedForAll true for operator", func(t *testing.T) {
    650 uassert.True(t, tok.IsApprovedForAll(alice, bob))
    651 }},
    652 {"IsApprovedForAll false for non-operator", func(t *testing.T) {
    653 uassert.False(t, tok.IsApprovedForAll(alice, carl))
    654 }},
    655 {"RenderHome renders header and counters", func(t *testing.T) {
    656 out := tok.RenderHome()
    657 uassert.True(t, strings.Contains(out, "# Foo NFT ($FOO)"), "header")
    658 uassert.True(t, strings.Contains(out, "**Total supply**: 3"), "supply")
    659 uassert.True(t, strings.Contains(out, "**Known accounts**: 2"), "accounts")
    660 }},
    661 }
    662
    663 for _, tc := range tests {
    664 tc := tc
    665 t.Run(tc.name, tc.check)
    666 }
    667}
    668
    669func TestRegisterExtensionNil(cur realm, t *testing.T) {
    670 _, led := newTestToken("Foo", "FOO", 0, cur)
    671 uassert.PanicsWithMessage(t, cur, "grc721: nil extension", func() {
    672 led.RegisterExtension(nil)
    673 })
    674}
    675
    676func TestRegisterExtensionRejectsDuplicateKind(cur realm, t *testing.T) {
    677 _, led := newTestToken("Foo", "FOO", 0, cur)
    678 ext := &mockExtension{}
    679 led.RegisterExtension(ext)
    680
    681 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: mock", func() {
    682 led.RegisterExtension(ext)
    683 })
    684 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: mock", func() {
    685 led.RegisterExtension(&mockExtension{})
    686 })
    687}
    688
    689func TestRegisterExtensionRejectsDuplicateKindOfUncomparableType(cur realm, t *testing.T) {
    690 _, led := newTestToken("Foo", "FOO", 0, cur)
    691 led.RegisterExtension(journalExtension{seen: []TokenID{"1"}})
    692
    693 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: journal", func() {
    694 led.RegisterExtension(journalExtension{seen: []TokenID{"2"}})
    695 })
    696}
    697
    698func TestExtensionHookFanOut(cur realm, t *testing.T) {
    699 tests := []struct {
    700 name string
    701 run func(t *testing.T, led *PrivateLedger, ext *mockExtension)
    702 verify func(t *testing.T, ext *mockExtension)
    703 }{
    704 {
    705 name: "mint, transfer and burn each notify the extension",
    706 run: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {
    707 led.RegisterExtension(ext)
    708 urequire.NoError(t, led.Mint(alice, "1"))
    709 urequire.NoError(t, led.TransferFrom(alice, alice, bob, "1"))
    710 urequire.NoError(t, led.Burn("1"))
    711 },
    712 verify: func(t *testing.T, ext *mockExtension) {
    713 uassert.Equal(t, 1, ext.mints)
    714 uassert.Equal(t, 1, ext.transfers)
    715 uassert.Equal(t, alice, ext.lastTransferFrom)
    716 uassert.Equal(t, bob, ext.lastTransferTo)
    717 uassert.Equal(t, 1, ext.burns)
    718 uassert.Equal(t, "1", ext.lastBurn.String())
    719 },
    720 },
    721 {
    722 name: "failed operations do not notify the extension",
    723 run: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {
    724 led.RegisterExtension(ext)
    725 uassert.ErrorIs(t, led.Mint(zeroAddress, "1"), ErrInvalidAddress)
    726 uassert.ErrorIs(t, led.Burn("999"), ErrInvalidTokenId)
    727 },
    728 verify: func(t *testing.T, ext *mockExtension) {
    729 uassert.Equal(t, 0, ext.mints)
    730 uassert.Equal(t, 0, ext.burns)
    731 },
    732 },
    733 }
    734
    735 for _, tc := range tests {
    736 tc := tc
    737 t.Run(tc.name, func(t *testing.T) {
    738 _, led := newTestToken("Foo", "FOO", 0, cur)
    739 ext := &mockExtension{}
    740 tc.run(t, led, ext)
    741 tc.verify(t, ext)
    742 })
    743 }
    744}
    745
    746func TestReadToken(cur realm, t *testing.T) {
    747 tok, led := newTestToken("Foo", "FOO", 0, cur)
    748 uassert.Equal(t, tok.ID(), led.ReadToken().ID())
    749 uassert.True(t, led.ReadToken() == tok, "ReadToken returns the same *Token")
    750}
    751