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

token_test.gno

22.90 Kb · 750 lines
  1package grc721
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"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)
 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}