Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}