const MaxNameLen, MaxSymbolLen
Symbol charset matches grc20reg slug (embedded in Token.ID / events).
Symbol charset matches grc20reg slug (embedded in Token.ID / events).
1const (
2 // NewToken announces every token creation; see NewToken for why it is a
3 // complete provenance signal.
4 NewTokenEvent = "NewToken"
5 // Mint emits from empty addr; burn emits to empty addr (EIP-721).
6 TransferEvent = "Transfer"
7 ApprovalEvent = "Approval"
8 ApprovalForAllEvent = "ApprovalForAll"
9) 1var (
2 ErrInvalidTokenId = errors.New("invalid token id")
3 ErrInvalidAddress = errors.New("invalid address")
4 ErrTokenIdNotApproved = errors.New("token id not approved for anyone")
5 ErrApprovalToCurrentOwner = errors.New("approval to current owner")
6 ErrCallerIsNotOwner = errors.New("caller is not token owner")
7 ErrCannotTransferToSelf = errors.New("cannot send transfer to self")
8 ErrTransferFromIncorrectOwner = errors.New("transfer from incorrect owner")
9 ErrCallerIsNotOwnerOrApproved = errors.New("caller is not token owner or approved")
10 ErrTokenIdAlreadyExists = errors.New("token id already exists")
11 ErrReadonly = errors.New("teller is readonly")
12 ErrSpoofedRealm = errors.New("rlm does not match the current crossing frame")
13 ErrForeignCallerTeller = errors.New("frame-relative teller used outside the token's realm")
14 ErrNotRealm = errors.New("rlm must be a realm (got EOA/origin)")
15 ErrInvalidName = errors.New("invalid token name (empty, too long, or contains control chars)")
16 ErrInvalidSymbol = errors.New("invalid token symbol (empty, too long, or contains chars outside [A-Za-z0-9_-])")
17)IsCanonicalTeller reports whether t was minted by this package, rejecting Tellers forged by embedding *fnTeller in a wrapper. It does NOT discriminate by capability grade: CallerTeller, ReadonlyTeller, RealmTeller, RealmSubTeller and the admin-grade ImpersonateTeller all return true. Do not treat a true result as proof that a Teller is a caller-scoped capability — establish the acting account separately.
NewToken creates a core token and its ledger. rlm must be the caller's own captured cur (IsCurrent); its PkgPath becomes the unforgeable origRealm.
Every successful call emits a NewToken event carrying the resulting Token.ID(). Because Token's fields are unexported, NewToken is the only way a Token can come into existence, so this event makes token creation fully observable: an indexer that sees the same Token.ID() announced twice knows the realm built two independent ledgers behind one identifier, and that every later Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm is emitting untrustworthy events and should be flagged or ignored wholesale.
Extension hooks fire on every mint/transfer/burn. This is a trust grant to the issuer, not an EIP-721 feature: only the issuer can attach one (RegisterExtension holds the PrivateLedger), but once attached the hook runs arbitrary issuer code on every movement, a panic in OnMint/OnTransfer/OnBurn aborts the transaction before the movement is announced — an implicit veto over transfers — and gas scales linearly with the extension count. Hooks run after the ledger write so they observe the post-movement state; only a caller that recovers from the panic keeps that write. Holders of a collection's tokens are therefore trusting the issuer not to freeze or tax movement through this surface. Hooks take no rlm params (they cannot capture cur); attach only via RegisterExtension.
caller must own tid or be an approved operator of the owner. EIP-721: approving the zero address clears (revokes) any single-token approval.
Fans OnBurn out to every extension so per-token state cannot resurface if tid is re-minted.
CallerTeller resolves the acting account at each write as rlm.Previous() — the realm that crossed into the realm holding the teller.
SECURITY: this accessor hangs off *PrivateLedger, not *Token, and that is load-bearing. A frame-relative teller acts as whoever crossed into its holder, so it is only meaningful inside the token's own realm, where the wrappers act for a caller who knowingly invoked the collection. Anywhere else it is a confused deputy: a realm a user merely calls could move that user's tokens. The *Token pointer is published — exported vars, the collection facade, grc721reg — while the ledger is not, since NewToken hands it to the creating realm and nowhere else. So a foreign realm cannot mint one.
Construction privacy alone is not enough: a realm may legally build a teller and then export the VALUE. The write methods therefore also verify that the invoking realm is the token's own (see guardHome), which leaves a leaked teller inert everywhere but home.
Admin-grade: issuer-only (holds the PrivateLedger). Panics on invalid addr to close the empty-sentinel hole.
RegisterExtension attaches an issuer-supplied hook set. Issuer-only (holds the PrivateLedger), which keeps the surface from being attacker-attachable — but the attachment is still a privilege: per the Extension doc, an attached hook can veto transfers (a panic aborts the transaction before the Transfer event is emitted) and adds gas to every movement. One extension per kind; a second registration of an attached kind panics.
spender must own or be approved for tid; callers derive it from a trusted per-frame identity.
1type Teller interface {
2 GetName() string
3 GetSymbol() string
4 ID() string
5 TotalSupply() int64
6 BalanceOf(owner address) (int64, error)
7 OwnerOf(tid TokenID) (address, error)
8 GetApproved(tid TokenID) (address, error)
9 IsApprovedForAll(owner, operator address) bool
10
11 Approve(_ int, rlm realm, to address, tid TokenID) error
12 SetApprovalForAll(_ int, rlm realm, operator address, approved bool) error
13 TransferFrom(_ int, rlm realm, from, to address, tid TokenID) error
14}A Teller is a capability that acts as some account whenever it writes. IsCanonicalTeller confirms a Teller was minted by this package — an embedding forgery fails the check — but it does NOT reveal which account the Teller acts as: a caller-scoped CallerTeller and the admin-grade ImpersonateTeller (which acts as an arbitrary address) are both canonical. So it is an authenticity check on the implementation, not an authorization guard on the acting account; a caller accepting a Teller from outside must still establish, out of band, what account that Teller is entitled to act as. safeTransferFrom is omitted: EIP-721's receiver check needs a registry, and aliasing it would imply false safety.
EIP-721: balanceOf throws for queries about the zero address, since NFTs assigned to it are considered invalid.
EIP-721: getApproved throws for a token that is not a valid NFT, so a missing token is ErrInvalidTokenId and a live but unapproved one is ErrTokenIdNotApproved.
Write methods return ErrReadonly.
Like RealmTeller but acts as a sub-account derived from slug.
Permanently acts as the calling realm (verified via IsCurrent).