1package grc72123import (4 "chain"5 "math/overflow"6 "strconv"78 "gno.land/p/nt/seqid/v0"9 "gno.land/p/nt/ufmt/v0"10)1112// NewToken creates a core token and its ledger.13// rlm must be the caller's own captured cur (IsCurrent); its PkgPath becomes the unforgeable origRealm.14//15// Every successful call emits a NewToken event carrying the resulting Token.ID().16// Because Token's fields are unexported, NewToken is the only way a Token can come17// into existence, so this event makes token creation fully observable: an indexer18// that sees the same Token.ID() announced twice knows the realm built two19// independent ledgers behind one identifier, and that every later20// Mint/Burn/Transfer/Approval carrying that id is ambiguous. Such a realm is21// emitting untrustworthy events and should be flagged or ignored wholesale.22func NewToken(name, symbol string, id seqid.ID, rlm realm) (*Token, *PrivateLedger) {23 if !rlm.IsCurrent() {24 panic(ErrSpoofedRealm)25 }2627 pkgPath := rlm.PkgPath()28 if pkgPath == "" {29 panic(ErrNotRealm)30 }3132 if !validName(name) {33 panic(ErrInvalidName)34 }3536 if !validSymbol(symbol) {37 panic(ErrInvalidSymbol)38 }3940 // origRealm is the host path, with any "#subpath" synthesized by realm.Sub41 // stripped. guardHome strips the invoking path the same way, so a token42 // created from a sub frame is not locked out of its own host realm, and a43 // host's sub-realms are not falsely rejected. Token.ID() keeps the raw44 // pkgPath, so identities and the NewToken event are unaffected.45 origRealm, _, _ := splitPkgSubPath(pkgPath)4647 ledger := &PrivateLedger{}48 token := &Token{49 id: pkgPath + "." + symbol + "." + id.String(),50 name: name,51 symbol: symbol,52 ledger: ledger,53 origRealm: origRealm,54 }55 ledger.token = token5657 chain.Emit(58 NewTokenEvent,59 "token", token.id,60 "name", name,61 "symbol", symbol,62 )6364 return token, ledger65}6667func validName(name string) bool {68 if name == "" || len(name) > MaxNameLen {69 return false70 }7172 for _, c := range name {73 if c < 0x20 || c == 0x7f {74 return false75 }76 }7778 return true79}8081func validSymbol(s string) bool {82 if s == "" || len(s) > MaxSymbolLen {83 return false84 }8586 for _, c := range s {87 if !isAlnum(c) && c != '_' && c != '-' {88 return false89 }90 }9192 return true93}9495func isAlnum(c rune) bool {96 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')97}9899func (tok *Token) GetName() string { return tok.name }100101func (tok *Token) GetSymbol() string { return tok.symbol }102103func (tok *Token) ID() string { return tok.id }104105func (tok *Token) TotalSupply() int64 { return tok.ledger.totalSupply }106107func (tok *Token) KnownAccounts() int { return tok.ledger.balances.Size() }108109// EIP-721: balanceOf throws for queries about the zero address,110// since NFTs assigned to it are considered invalid.111func (tok *Token) BalanceOf(addr address) (int64, error) {112 if !addr.IsValid() {113 return 0, ErrInvalidAddress114 }115116 return tok.ledger.balanceOf(addr), nil117}118119func (tok *Token) OwnerOf(tid TokenID) (address, error) { return tok.ledger.ownerOf(tid) }120121// EIP-721: getApproved throws for a token that is not a valid NFT, so a missing122// token is ErrInvalidTokenId and a live but unapproved one is ErrTokenIdNotApproved.123func (tok *Token) GetApproved(tid TokenID) (address, error) {124 if !tok.ledger.exists(tid) {125 return zeroAddress, ErrInvalidTokenId126 }127128 addr := tok.ledger.tokenApprovals.Get(tid.String())129 if addr == nil {130 return zeroAddress, ErrTokenIdNotApproved131 }132133 return addr.(address), nil134}135136func (tok *Token) IsApprovedForAll(owner, operator address) bool {137 return tok.ledger.isApprovedForAll(owner, operator)138}139140func (tok *Token) RenderHome() string {141 str := ""142 str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol)143 str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply)144 str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts())145146 return str147}148149// RegisterExtension attaches an issuer-supplied hook set. Issuer-only (holds the150// PrivateLedger), which keeps the surface from being attacker-attachable — but the151// attachment is still a privilege: per the Extension doc, an attached hook can veto152// transfers (a panic aborts the transaction before the Transfer event is emitted) and153// adds gas to every movement.154// One extension per kind; a second registration of an attached kind panics.155func (led *PrivateLedger) RegisterExtension(ext Extension) {156 if ext == nil {157 panic("grc721: nil extension")158 }159160 kind := ext.ExtensionKind()161 for _, e := range led.extensions {162 if e.ExtensionKind() == kind {163 panic("grc721: extension kind already registered: " + kind)164 }165 }166167 led.extensions = append(led.extensions, ext)168}169170func (led *PrivateLedger) ReadToken() *Token { return led.token }171172func (led *PrivateLedger) Mint(to address, tid TokenID) error {173 if !to.IsValid() {174 return ErrInvalidAddress175 }176177 if tid == "" {178 return ErrInvalidTokenId179 }180181 if led.exists(tid) {182 return ErrTokenIdAlreadyExists183 }184185 led.balances.Set(to.String(), overflow.Add64p(led.balanceOf(to), 1))186 led.owners.Set(tid.String(), to)187 led.totalSupply = overflow.Add64p(led.totalSupply, 1)188189 for _, ext := range led.extensions {190 ext.OnMint(to, tid)191 }192193 // Mint emits Transfer from the empty address (EIP-721); indexers reconstruct ownership from Transfer alone.194 chain.Emit(195 TransferEvent,196 "token", led.token.id,197 "from", "",198 "to", to.String(),199 "tokenId", tid.String(),200 )201202 return nil203}204205// Fans OnBurn out to every extension so per-token state cannot resurface if tid is re-minted.206func (led *PrivateLedger) Burn(tid TokenID) error {207 owner, err := led.ownerOf(tid)208 if err != nil {209 return err210 }211212 tidStr := tid.String()213 led.tokenApprovals.Remove(tidStr)214 led.setBalance(owner, overflow.Sub64p(led.balanceOf(owner), 1))215 led.owners.Remove(tidStr)216 led.totalSupply = overflow.Sub64p(led.totalSupply, 1)217218 for _, ext := range led.extensions {219 ext.OnBurn(tid)220 }221222 // Burn emits Transfer to the empty address (EIP-721).223 chain.Emit(224 TransferEvent,225 "token", led.token.id,226 "from", owner.String(),227 "to", "",228 "tokenId", tidStr,229 )230231 return nil232}233234// spender must own or be approved for tid; callers derive it from a trusted per-frame identity.235func (led *PrivateLedger) TransferFrom(spender, from, to address, tid TokenID) error {236 if !led.isApprovedOrOwner(spender, tid) {237 return ErrCallerIsNotOwnerOrApproved238 }239240 return led.transfer(from, to, tid)241}242243// caller must own tid or be an approved operator of the owner.244// EIP-721: approving the zero address clears (revokes) any single-token approval.245func (led *PrivateLedger) Approve(caller, to address, tid TokenID) error {246 revoke := to == zeroAddress247 if !revoke && !to.IsValid() {248 return ErrInvalidAddress249 }250251 owner, err := led.ownerOf(tid)252 if err != nil {253 return err254 }255256 if owner == to {257 return ErrApprovalToCurrentOwner258 }259260 if caller != owner && !led.isApprovedForAll(owner, caller) {261 return ErrCallerIsNotOwnerOrApproved262 }263264 if revoke {265 led.tokenApprovals.Remove(tid.String())266 } else {267 led.tokenApprovals.Set(tid.String(), to)268 }269270 chain.Emit(271 ApprovalEvent,272 "token", led.token.id,273 "owner", owner.String(),274 "to", to.String(), // empty for a revoke275 "tokenId", tid.String(),276 )277278 return nil279}280281func (led *PrivateLedger) SetApprovalForAll(owner, operator address, approved bool) error {282 if !owner.IsValid() || !operator.IsValid() {283 return ErrInvalidAddress284 }285286 if owner == operator {287 return ErrApprovalToCurrentOwner288 }289290 key := operatorKey(owner, operator)291 if approved {292 led.operatorApprovals.Set(key, approved)293 } else {294 led.operatorApprovals.Remove(key)295 }296297 chain.Emit(298 ApprovalForAllEvent,299 "token", led.token.id,300 "owner", owner.String(),301 "to", operator.String(),302 "approved", strconv.FormatBool(approved),303 )304305 return nil306}307308func (led *PrivateLedger) transfer(from, to address, tid TokenID) error {309 if !from.IsValid() || !to.IsValid() {310 return ErrInvalidAddress311 }312313 if from == to {314 return ErrCannotTransferToSelf315 }316317 owner, err := led.ownerOf(tid)318 if err != nil {319 return err320 }321322 if owner != from {323 return ErrTransferFromIncorrectOwner324 }325326 tidStr := tid.String()327 led.tokenApprovals.Remove(tidStr)328 led.setBalance(from, overflow.Sub64p(led.balanceOf(from), 1))329 led.setBalance(to, overflow.Add64p(led.balanceOf(to), 1))330 led.owners.Set(tidStr, to)331332 for _, ext := range led.extensions {333 ext.OnTransfer(from, to, tid)334 }335336 chain.Emit(337 TransferEvent,338 "token", led.token.id,339 "from", from.String(),340 "to", to.String(),341 "tokenId", tidStr,342 )343344 return nil345}346347func (led *PrivateLedger) ownerOf(tid TokenID) (address, error) {348 owner := led.owners.Get(tid.String())349 if owner == nil {350 return zeroAddress, ErrInvalidTokenId351 }352353 return owner.(address), nil354}355356func (led *PrivateLedger) balanceOf(addr address) int64 {357 balance := led.balances.Get(addr.String())358 if balance == nil {359 return 0360 }361362 return balance.(int64)363}364365func (led *PrivateLedger) setBalance(addr address, balance int64) {366 if balance == 0 {367 led.balances.Remove(addr.String())368369 return370 }371372 led.balances.Set(addr.String(), balance)373}374375func (led *PrivateLedger) isApprovedForAll(owner, operator address) bool {376 approved := led.operatorApprovals.Get(operatorKey(owner, operator))377 if approved == nil {378 return false379 }380381 return approved.(bool)382}383384func (led *PrivateLedger) isApprovedOrOwner(addr address, tid TokenID) bool {385 owner := led.owners.Get(tid.String())386 if owner == nil {387 return false388 }389390 ownerAddr := owner.(address)391 if addr == ownerAddr || led.isApprovedForAll(ownerAddr, addr) {392 return true393 }394395 approved := led.tokenApprovals.Get(tid.String())396397 return approved != nil && approved.(address) == addr398}399400func (led *PrivateLedger) exists(tid TokenID) bool {401 return led.owners.Has(tid.String())402}403404func operatorKey(owner, operator address) string {405 return owner.String() + ":" + operator.String()406}407Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.