types.gno
5.42 Kb · 133 lines
1package grc721
2
3import (
4 "errors"
5
6 "gno.land/p/nt/avl/v0"
7)
8
9type TokenID string
10
11func (t TokenID) String() string { return string(t) }
12
13// A Teller is a capability that acts as some account whenever it writes.
14// IsCanonicalTeller confirms a Teller was minted by this package — an embedding
15// forgery fails the check — but it does NOT reveal which account the Teller acts
16// as: a caller-scoped CallerTeller and the admin-grade ImpersonateTeller (which
17// acts as an arbitrary address) are both canonical. So it is an authenticity
18// check on the implementation, not an authorization guard on the acting account;
19// a caller accepting a Teller from outside must still establish, out of band, what
20// account that Teller is entitled to act as.
21// safeTransferFrom is omitted: EIP-721's receiver check needs a registry, and aliasing it would imply false safety.
22type Teller interface {
23 GetName() string
24 GetSymbol() string
25 ID() string
26 TotalSupply() int64
27 BalanceOf(owner address) (int64, error)
28 OwnerOf(tid TokenID) (address, error)
29 GetApproved(tid TokenID) (address, error)
30 IsApprovedForAll(owner, operator address) bool
31
32 Approve(_ int, rlm realm, to address, tid TokenID) error
33 SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error
34 TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error
35}
36
37// Extension hooks fire on every mint/transfer/burn. This is a trust grant to the
38// issuer, not an EIP-721 feature: only the issuer can attach one (RegisterExtension
39// holds the PrivateLedger), but once attached the hook runs arbitrary issuer code on
40// every movement, a panic in OnMint/OnTransfer/OnBurn aborts the transaction before
41// the movement is announced — an implicit veto over transfers — and gas scales
42// linearly with the extension count. Hooks run after the ledger write so they observe
43// the post-movement state; only a caller that recovers from the panic keeps that write.
44// Holders of a collection's tokens are therefore trusting the issuer not to freeze or
45// tax movement through this surface. Hooks take no rlm params (they cannot capture cur);
46// attach only via RegisterExtension.
47type Extension interface {
48 ExtensionKind() string // unique; duplicates rejected at register
49 OnMint(to address, tid TokenID)
50 OnTransfer(from, to address, tid TokenID)
51 OnBurn(tid TokenID)
52}
53
54type ExtensionView interface {
55 ExtensionKind() string
56 TokenID() string // core Token.ID
57}
58
59type Token struct {
60 id string // origRealm + "." + symbol + "." + id
61 name string
62 symbol string
63 ledger *PrivateLedger
64 // origRealm is the PkgPath of the realm that created the token, captured
65 // unforgeably in NewToken. A frame-relative teller only works there.
66 origRealm string
67}
68
69type PrivateLedger struct {
70 token *Token
71 totalSupply int64
72 owners avl.Tree // TokenID -> owner address
73 balances avl.Tree // owner address -> int64
74 tokenApprovals avl.Tree // TokenID -> approved address
75 operatorApprovals avl.Tree // "owner:operator" -> bool
76 extensions []Extension
77}
78
79var (
80 ErrInvalidTokenId = errors.New("invalid token id")
81 ErrInvalidAddress = errors.New("invalid address")
82 ErrTokenIdNotApproved = errors.New("token id not approved for anyone")
83 ErrApprovalToCurrentOwner = errors.New("approval to current owner")
84 ErrCallerIsNotOwner = errors.New("caller is not token owner")
85 ErrCannotTransferToSelf = errors.New("cannot send transfer to self")
86 ErrTransferFromIncorrectOwner = errors.New("transfer from incorrect owner")
87 ErrCallerIsNotOwnerOrApproved = errors.New("caller is not token owner or approved")
88 ErrTokenIdAlreadyExists = errors.New("token id already exists")
89 ErrReadonly = errors.New("teller is readonly")
90 ErrSpoofedRealm = errors.New("rlm does not match the current crossing frame")
91 ErrForeignCallerTeller = errors.New("frame-relative teller used outside the token's realm")
92 ErrNotRealm = errors.New("rlm must be a realm (got EOA/origin)")
93 ErrInvalidName = errors.New("invalid token name (empty, too long, or contains control chars)")
94 ErrInvalidSymbol = errors.New("invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])")
95)
96
97// Symbol charset matches grc20reg slug (embedded in Token.ID / events).
98const (
99 MaxNameLen = 64
100 MaxSymbolLen = 11
101)
102
103const (
104 // NewToken announces every token creation; see NewToken for why it is a
105 // complete provenance signal.
106 NewTokenEvent = "NewToken"
107 // Mint emits from empty addr; burn emits to empty addr (EIP-721).
108 TransferEvent = "Transfer"
109 ApprovalEvent = "Approval"
110 ApprovalForAllEvent = "ApprovalForAll"
111)
112
113var zeroAddress = address("")
114
115type fnTeller struct {
116 accountFn func(_ int, rlm realm) address
117 // homeGuard marks a frame-relative teller, whose writes are confined to
118 // the token's own realm. See fnTeller.guardHome.
119 homeGuard bool
120 *Token
121}
122
123var _ Teller = (*fnTeller)(nil)
124
125// IsCanonicalTeller reports whether t was minted by this package, rejecting Tellers
126// forged by embedding *fnTeller in a wrapper. It does NOT discriminate by capability
127// grade: CallerTeller, ReadonlyTeller, RealmTeller, RealmSubTeller and the admin-grade
128// ImpersonateTeller all return true. Do not treat a true result as proof that a Teller
129// is a caller-scoped capability — establish the acting account separately.
130func IsCanonicalTeller(t Teller) bool {
131 _, ok := t.(*fnTeller)
132 return ok
133}