1package royalty23import (4 "chain"5 "math"6 "strconv"78 "gno.land/p/nt/grc721/v0"9)1011const Kind = "royalty"1213// NewRoyalty attaches a royalty extension; maxBps caps every rate and must be 0..10000.14func NewRoyalty(coreLedger *grc721.PrivateLedger, maxBps int64) (*Royalty, *Ledger) {15 if coreLedger == nil {16 panic("royalty: nil core ledger")17 }1819 if maxBps < 0 || maxBps > FeeDenominator {20 panic(ErrMaxBpsRange)21 }2223 st := &storage{maxBps: maxBps}24 core := coreLedger.ReadToken()25 led := &Ledger{core: core, st: st}2627 coreLedger.RegisterExtension(led)2829 return &Royalty{core: core, st: st}, led30}3132func (r *Royalty) ExtensionKind() string { return Kind }3334func (r *Royalty) TokenID() string { return r.core.ID() }3536func (r *Royalty) MaxBps() int64 { return r.st.maxBps }3738// RoyaltyInfo implements EIP-2981; per-token override takes precedence over the39// default, and no royalty returns (zeroAddress, 0) — the "no royalty" signal.40func (r *Royalty) RoyaltyInfo(tid grc721.TokenID, salePrice int64) (address, int64, error) {41 if salePrice < 0 {42 return zeroAddress, 0, ErrInvalidSalePrice43 }4445 info, ok := r.resolve(tid)46 if !ok {47 return zeroAddress, 0, nil48 }4950 if info.Bps != 0 && salePrice > math.MaxInt64/info.Bps {51 return zeroAddress, 0, ErrInvalidSalePrice52 }5354 amount := salePrice * info.Bps / FeeDenominator5556 return info.Receiver, amount, nil57}5859func (r *Royalty) DefaultRoyalty() (RoyaltyInfo, bool) {60 if !r.st.hasDefault {61 return RoyaltyInfo{}, false62 }6364 return r.st.def, true65}6667func (r *Royalty) TokenRoyalty(tid grc721.TokenID) (RoyaltyInfo, bool) {68 v := r.st.perToken.Get(tid.String())69 if v == nil {70 return RoyaltyInfo{}, false71 }7273 return v.(RoyaltyInfo), true74}7576// resolve returns the effective royalty: per-token override first, then default.77func (r *Royalty) resolve(tid grc721.TokenID) (RoyaltyInfo, bool) {78 if v := r.st.perToken.Get(tid.String()); v != nil {79 return v.(RoyaltyInfo), true80 }8182 if r.st.hasDefault {83 return r.st.def, true84 }8586 return RoyaltyInfo{}, false87}8889// SetDefaultRoyalty sets the collection default for tokens without an override; issuer-only.90func (led *Ledger) SetDefaultRoyalty(receiver address, bps int64) error {91 if err := led.validate(receiver, bps); err != nil {92 return err93 }9495 led.st.def = RoyaltyInfo{Receiver: receiver, Bps: bps}96 led.st.hasDefault = true9798 led.emitRoyaltyUpdate("default", "", receiver, bps)99 return nil100}101102func (led *Ledger) DeleteDefaultRoyalty() {103 led.st.hasDefault = false104 led.st.def = RoyaltyInfo{}105106 led.emitRoyaltyUpdate("default", "", zeroAddress, 0)107}108109// SetTokenRoyalty sets a per-token override for tid; issuer-only.110func (led *Ledger) SetTokenRoyalty(tid grc721.TokenID, receiver address, bps int64) error {111 if _, err := led.core.OwnerOf(tid); err != nil {112 return ErrTokenNotMinted113 }114115 if err := led.validate(receiver, bps); err != nil {116 return err117 }118119 led.st.perToken.Set(tid.String(), RoyaltyInfo{Receiver: receiver, Bps: bps})120121 led.emitRoyaltyUpdate("token", tid.String(), receiver, bps)122 return nil123}124125// DeleteTokenRoyalty clears the per-token override, falling back to the default.126func (led *Ledger) DeleteTokenRoyalty(tid grc721.TokenID) {127 led.st.perToken.Remove(tid.String())128129 led.emitRoyaltyUpdate("token", tid.String(), zeroAddress, 0)130}131132// emitRoyaltyUpdate signals a royalty change (EIP-2981 defines no events); a133// cleared royalty is reported as an empty receiver and bps 0.134func (led *Ledger) emitRoyaltyUpdate(scope, tokenID string, receiver address, bps int64) {135 chain.Emit(136 RoyaltyUpdateEvent,137 "token", led.core.ID(),138 "scope", scope,139 "tokenId", tokenID,140 "receiver", receiver.String(),141 "bps", strconv.FormatInt(bps, 10),142 )143}144145func (led *Ledger) validate(receiver address, bps int64) error {146 if !receiver.IsValid() {147 return ErrInvalidReceiver148 }149150 if bps < 0 || bps > led.st.maxBps {151 return ErrInvalidBps152 }153154 return nil155}156157func (led *Ledger) ExtensionKind() string { return Kind }158159func (led *Ledger) OnMint(to address, tid grc721.TokenID) {}160func (led *Ledger) OnTransfer(from, to address, tid grc721.TokenID) {}161162// OnBurn clears the per-token override (cannot resurface on re-mint) but keeps163// the default, which is collection-level policy, not per-token state.164func (led *Ledger) OnBurn(tid grc721.TokenID) {165 led.st.perToken.Remove(tid.String())166}167Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.