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

skins.gno

5.00 Kb · 178 lines
  1package skins
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/nt/bptree/v0"
  9	"gno.land/p/nt/grc721/v0"
 10)
 11
 12// design is one skin of the catalog: what it looks like and how it sells.
 13type design struct {
 14	id        string
 15	name      string
 16	model     string // a model the client draws (web/src/world/planes.ts)
 17	pilot     string // none or gnome
 18	body      string // colours, "#rrggbb"
 19	wing      string
 20	accent    string
 21	price     int64 // ugnot
 22	maxSupply int64 // how many tokens of it can ever be minted, 0 for no limit
 23	minted    int64 // tokens minted so far, burned ones included
 24	listed    bool  // an unlisted design cannot be bought, owners keep theirs
 25}
 26
 27// The state. Nothing is exported by reference: the ledger and the trees are
 28// /p/ types with mutator methods. The token itself is not exported either:
 29// its tellers move tokens, and a skin never moves (soulbound, see doc.gno).
 30var (
 31	token  *grc721.Token
 32	ledger *grc721.PrivateLedger
 33
 34	designs bptree.BPTree // design id -> *design
 35
 36	// Per token, as little as possible: the skins are bought for a few GNOT
 37	// and every byte is a deposit. The ledger keeps the owner; tokenSkin the
 38	// design, as a string; owned answers "does addr own design x" and lists
 39	// the designs of addr, keyed "<addr>/<design>", value the token id.
 40	tokenSkin bptree.BPTree // token id -> design id
 41	owned     bptree.BPTree // "<addr>/<design>" -> token id
 42
 43	nextID int64 // the id of the next token, 1 first
 44
 45	webPath string // "/r/.../gnofly/nfts/skins", for the links of Render
 46)
 47
 48const (
 49	tokenName   = "gnofly skins"
 50	tokenSymbol = "SKIN"
 51	payDenom    = "ugnot"
 52	maxPrice    = int64(1000000000000)
 53
 54	// uriBase is where the website serves the metadata of each design: a
 55	// token's URI is <uriBase><design>.json. It is computed on read, not
 56	// stored per token.
 57	uriBase = "https://gnofly.xyz/nft/skins/"
 58
 59	classic = "classic" // the free look, never a token
 60)
 61
 62func init(cur realm) {
 63	bootstrap(0, cur)
 64}
 65
 66// bootstrap (re)creates the state and seeds the catalog. The tests call it to
 67// start over.
 68func bootstrap(_ int, rlm realm) {
 69	token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm)
 70	designs = bptree.BPTree{}
 71	tokenSkin = bptree.BPTree{}
 72	owned = bptree.BPTree{}
 73	nextID = 1
 74	webPath = webPathOf(rlm.PkgPath())
 75	for _, line := range strings.Split(catalog, "\n") {
 76		f := strings.Split(line, "|")
 77		if len(f) != 8 {
 78			panic(errBadCatalog)
 79		}
 80		price, err := strconv.ParseInt(f[7], 10, 64)
 81		if err != nil {
 82			panic(errBadCatalog)
 83		}
 84		putDesign(f[0], f[1], f[2], f[3], f[4], f[5], f[6], price, 0)
 85	}
 86}
 87
 88// getDesign returns the design registered under id, or nil.
 89func getDesign(id string) *design {
 90	v := designs.Get(id)
 91	if v == nil {
 92		return nil
 93	}
 94	return v.(*design)
 95}
 96
 97// putDesign validates and stores a design. An existing one keeps its count
 98// and its listed flag; a new one starts listed.
 99func putDesign(id, name, model, pilot, body, wing, accent string, price, maxSupply int64) {
100	if !validID(id) {
101		panic(errInvalidID)
102	}
103	if id == classic {
104		panic(errReservedID)
105	}
106	if !validName(name) {
107		panic(errInvalidName)
108	}
109	if !validModel(model) {
110		panic(errInvalidModel)
111	}
112	if pilot != "none" && pilot != "gnome" {
113		panic(errInvalidPilot)
114	}
115	if !validColor(body) || !validColor(wing) || !validColor(accent) {
116		panic(errInvalidColor)
117	}
118	if price < 1 || price > maxPrice {
119		panic(errInvalidPrice)
120	}
121	if maxSupply < 0 {
122		panic(errInvalidSupply)
123	}
124	d := &design{id: id, listed: true}
125	if old := getDesign(id); old != nil {
126		d.minted, d.listed = old.minted, old.listed
127	}
128	d.name, d.model, d.pilot = name, model, pilot
129	d.body, d.wing, d.accent = body, wing, accent
130	d.price, d.maxSupply = price, maxSupply
131	designs.Set(id, d)
132}
133
134// SetSkin creates or updates a design. Admin only.
135//
136// id is [a-z0-9-]{1,32} and not "classic", name [A-Za-z0-9 '.!-]{1,40}, model
137// [a-z0-9-]{1,16}, pilot none or gnome, colours "#rrggbb" in lowercase, price
138// 1 to 1e12 ugnot, maxSupply 0 for unlimited. A max supply under what is
139// already minted stops the sales, it takes nothing back. Owners keep their
140// tokens whatever happens to the design; its look is read from here, so a
141// change of colours repaints every copy.
142func SetSkin(cur realm, id, name, model, pilot, body, wing, accent string, price, maxSupply int64) {
143	assertAdmin(0, cur)
144	putDesign(id, name, model, pilot, body, wing, accent, price, maxSupply)
145	chain.Emit("SkinSet", "id", id, "price", itoa(price), "maxSupply", itoa(maxSupply))
146}
147
148// SetSkinListed puts a design on or off sale. Admin only.
149func SetSkinListed(cur realm, id string, listed bool) {
150	assertAdmin(0, cur)
151	d := getDesign(id)
152	if d == nil {
153		panic(errUnknownSkin)
154	}
155	d.listed = listed
156	chain.Emit("SkinListed", "id", id, "listed", boolString(listed))
157}
158
159// webPathOf turns "gno.land/r/..." into "/r/...".
160func webPathOf(pkgPath string) string {
161	for i := 0; i < len(pkgPath); i++ {
162		if pkgPath[i] == '/' {
163			return pkgPath[i:]
164		}
165	}
166	return ""
167}
168
169func itoa(n int64) string {
170	return strconv.FormatInt(n, 10)
171}
172
173func boolString(b bool) string {
174	if b {
175		return "true"
176	}
177	return "false"
178}