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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}