Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

terms.gno

4.57 Kb · 150 lines
  1package nft
  2
  3import (
  4	"chain"
  5	"strings"
  6
  7	sm "gno.land/p/samcrew/launchpad/safemath/v1"
  8	"gno.land/r/samcrew/launchpad/config/v1"
  9)
 10
 11// Hard limits, fixed for the life of this realm.
 12const (
 13	maxRoyaltyReceivers = 10
 14	maxRoyaltyBPS       = 1000 // 10% of a sale, across all receivers
 15	maxMarkets          = 5
 16)
 17
 18// royalty is one receiver's share of every sale, in basis points.
 19type royalty struct {
 20	Account address
 21	BPS     int64
 22}
 23
 24type RoyaltyPayout struct {
 25	Account address
 26	Amount  int64
 27}
 28
 29// parseBPS reads a positive decimal with no sign, space or leading zero.
 30func parseBPS(s string) int64 {
 31	if s == "" || len(s) > 4 || s[0] == '0' {
 32		panic("launchpad/nft: invalid royalty share")
 33	}
 34	var n int64
 35	for i := 0; i < len(s); i++ {
 36		if s[i] < '0' || s[i] > '9' {
 37			panic("launchpad/nft: invalid royalty share")
 38		}
 39		n = n*10 + int64(s[i]-'0')
 40	}
 41	return n
 42}
 43
 44// parseRoyalties reads "address:bps;address:bps". Strictly ascending addresses
 45// give every split exactly one encoding and rule out duplicates.
 46func parseRoyalties(encoded, mode string) ([]royalty, int64) {
 47	if encoded == "" {
 48		if mode == ModeRoyaltyProtected {
 49			panic("launchpad/nft: a royalty-protected collection needs royalties")
 50		}
 51		return nil, 0
 52	}
 53	if mode == ModeSoulbound {
 54		panic("launchpad/nft: soulbound tokens are never sold")
 55	}
 56	records := strings.Split(encoded, ";")
 57	if len(records) > maxRoyaltyReceivers {
 58		panic("launchpad/nft: too many royalty receivers")
 59	}
 60	out := make([]royalty, 0, len(records))
 61	previous := ""
 62	var total int64
 63	for _, record := range records {
 64		fields := strings.Split(record, ":")
 65		if len(fields) != 2 || fields[0] <= previous {
 66			panic("launchpad/nft: invalid royalty list")
 67		}
 68		who := address(fields[0])
 69		// Unspendable covers every text but the chain's spelling of an account.
 70		if config.IsUnspendable(who) {
 71			panic("launchpad/nft: invalid royalty receiver")
 72		}
 73		bps := parseBPS(fields[1])
 74		if total = sm.Add(total, bps); total > maxRoyaltyBPS {
 75			panic("launchpad/nft: royalties exceed 10%")
 76		}
 77		out = append(out, royalty{Account: who, BPS: bps})
 78		previous = fields[0]
 79	}
 80	return out, total
 81}
 82
 83// validName is the chain's rule for one segment of a package path: a letter
 84// first, then letters and digits, joined by single "_" or "-".
 85func validName(s string) bool {
 86	if s == "" || s[0] < 'a' || s[0] > 'z' {
 87		return false
 88	}
 89	joined := false // the previous character joins two runs
 90	for i := 1; i < len(s); i++ {
 91		switch c := s[i]; {
 92		case (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'):
 93			joined = false
 94		case (c == '_' || c == '-') && !joined:
 95			joined = true
 96		default:
 97			return false
 98		}
 99	}
100	return !joined
101}
102
103// A market may be any realm the chain can host, a third party's included, so
104// its path follows the chain's own rule for a realm path: "gno.land/r/", then
105// names separated by "/", none ending in "_test", in at most 200 bytes. A slot
106// is permanent: none is given to a path no realm can have. The rule for
107// issuers is config's and is narrower (plain segments in our own namespace):
108// an issuer is one of ours, a market need not be.
109func validMarketPath(path string) bool {
110	if len(path) > 200 || !strings.HasPrefix(path, "gno.land/r/") || strings.HasSuffix(path, "_test") {
111		return false
112	}
113	for _, name := range strings.Split(path[len("gno.land/r/"):], "/") {
114		if !validName(name) {
115			return false
116		}
117	}
118	return true
119}
120
121func RoyaltyBPS(id string) int64 { return mustCollection(id).royaltyBPS }
122
123// RoyaltyQuote floors each receiver's share of price. The seller keeps the
124// rounding remainder.
125func RoyaltyQuote(id string, price int64) []RoyaltyPayout {
126	royalties := mustCollection(id).royalties
127	out := make([]RoyaltyPayout, len(royalties))
128	for i, r := range royalties {
129		out[i] = RoyaltyPayout{Account: r.Account, Amount: sm.BPS(price, r.BPS)}
130	}
131	return out
132}
133
134// AllowMarket adds a realm through which a royalty-protected collection's
135// tokens may be sold. The list starts with the Launchpad market and only
136// grows: a holder's way out can be widened, never taken away. The ledger
137// cannot make a realm pay: the creator vouches that the market it lists pays
138// this collection's royalties, and every holder can read the list.
139func AllowMarket(cur realm, id, pkgPath string) {
140	c := creatorOf(cur, id)
141	if c.info.Mode != ModeRoyaltyProtected || !validMarketPath(pkgPath) ||
142		len(c.markets) >= maxMarkets ||
143		operator(c, chain.PackageAddress(pkgPath)) {
144		panic("launchpad/nft: market cannot be added")
145	}
146	market := chain.PackageAddress(pkgPath)
147	c.markets = append(c.markets, market)
148	chain.Emit("MarketAllowed", "collection", id, "market", pkgPath,
149		"address", market.String())
150}