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

planes.gno

7.77 Kb · 237 lines
  1package planes
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/nt/bptree/v0"
  9	"gno.land/p/nt/grc721/royalty/v0"
 10	"gno.land/p/nt/grc721/v0"
 11)
 12
 13// The read side of the collection, exported for other realms and for
 14// vm/qeval, as GRC721 tokens are on gno.land. Neither type has a mutator: a
 15// Token hands out tellers that act as the calling realm and go through the
 16// ledger's approval checks, Royalty only reads. There is no metadata
 17// extension: the traits are in the table, read with TokenMetadata(tid) and
 18// PlaneJSON(id), and the token URI, the image, is computed by TokenURI(tid).
 19//
 20//	planes.TokenMetadata("42")
 21//	planes.Token.RealmTeller(0, cur).TransferFrom(0, cur, from, to, "42")
 22var (
 23	Token     *grc721.Token    // name, symbol, owners, balances, approvals
 24	Royalties *royalty.Royalty // EIP-2981 royalty
 25)
 26
 27// The rest of the state. Everything is unexported and nothing below is ever
 28// returned by reference: the ledgers and the trees are /p/ types with mutator
 29// methods, so handing one out would be handing out write access to the
 30// collection.
 31var (
 32	ledger        *grc721.PrivateLedger // the collection, write side: mint, burn
 33	teller        grc721.Teller         // acts as the caller of this realm: transfers and approvals
 34	royaltyLedger *royalty.Ledger       // royalty, write side
 35
 36	mintCount int64  // planes minted so far, burned ones included; the ids left are in pool (draw.gno)
 37	price     int64  // ugnot, what a mint costs
 38	mintOpen  bool   // whether Mint is accepting calls
 39	mintCap   int64  // how many planes one wallet may mint, over its lifetime
 40	imageBase string // "https://gnofly.xyz/nft/collections/planes/", the image of plane 42 is at <base>042.png
 41
 42	mints      bptree.BPTree // minter address -> int64, planes minted so far, for the cap
 43	tierMinted [4]int64      // planes minted so far per tier, in the order of tiers
 44	recent     [5]int64      // the last ids minted, newest first, 0 for none, for Render
 45
 46	webPath string // "/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes", used to build links in Render
 47)
 48
 49// The collection, and the settings at deploy.
 50const (
 51	supply      = int64(777)
 52	tokenName   = "gnofly planes"
 53	tokenSymbol = "PLANE"
 54	payDenom    = "ugnot" // the only coin planes are sold for
 55
 56	defaultPrice     = int64(77000000) // 77 GNOT
 57	minPrice         = int64(1000000)
 58	maxPrice         = int64(10000000000)
 59	defaultMintCap   = int64(7)
 60	defaultImageBase = "https://gnofly.xyz/nft/collections/planes/"
 61
 62	defaultRoyaltyBps = int64(500)  // 5% of a resale, to the treasury
 63	maxRoyaltyBps     = int64(1000) // the most SetRoyaltyBps accepts
 64
 65	// Models are what the game client renders, tiers are what the table
 66	// says. Both are validated against these lists.
 67	models = "prop,biplane,stunt,jet,seaplane,twin,delta,glider,supergnome"
 68	pilots = "none,gnome"
 69	tiers  = "common,rare,epic,legendary"
 70)
 71
 72// tierSupply is how many planes of each tier the table holds, in the order
 73// of tiers. The tests check it against the table.
 74var tierSupply = [4]int64{500, 200, 70, 7}
 75
 76// tierIndex returns the position of tier in tiers, or -1.
 77func tierIndex(tier string) int {
 78	for i, t := range strings.Split(tiers, ",") {
 79		if t == tier {
 80			return i
 81		}
 82	}
 83	return -1
 84}
 85
 86// init runs once, when the package is added to the chain. The realm has
 87// nothing to remember about its deployer: it has no role of its own.
 88func init(cur realm) {
 89	bootstrap(0, cur)
 90}
 91
 92// bootstrap (re)creates the initial state: the token, its extensions and the
 93// settings. The tests call it to start from a clean slate. rlm is the live
 94// realm of the caller: grc721 records its package path as the home of the
 95// Token.
 96func bootstrap(_ int, rlm realm) {
 97	// A table of the wrong shape is refused before anything is written, so
 98	// that a bad hand edit fails at publish rather than at the first mint.
 99	if first, last := rowID(0), rowID(len(table)-1); first != 1 || last != supply {
100		panic(errBadTable)
101	}
102
103	Token, ledger = grc721.NewToken(tokenName, tokenSymbol, 0, rlm)
104	// Minted from the ledger: the frame-relative teller is confined to this
105	// realm, which is where it is used.
106	teller = ledger.CallerTeller()
107	Royalties, royaltyLedger = royalty.NewRoyalty(ledger, maxRoyaltyBps)
108
109	mintCount = 0
110	resetPool(rlm.PkgPath())
111	recent = [5]int64{}
112	price = defaultPrice
113	mintOpen = false
114	mintCap = defaultMintCap
115	imageBase = defaultImageBase
116	mints = bptree.BPTree{}
117	tierMinted = [4]int64{}
118	webPath = webPathOf(rlm.PkgPath())
119
120	// Under `gno test` the game realm has no treasury yet (see roles.gno):
121	// the tests set one and call bootstrap again.
122	if roles.Treasury().IsValid() {
123		setRoyalty(defaultRoyaltyBps)
124	}
125}
126
127// setRoyalty points the royalty of every plane at the treasury of the game
128// realm, as it is now, at bps basis points.
129func setRoyalty(bps int64) {
130	if err := royaltyLedger.SetDefaultRoyalty(roles.Treasury(), bps); err != nil {
131		panic(err)
132	}
133}
134
135// SetMintOpen opens or closes the mint. Admin only. The mint starts closed.
136func SetMintOpen(cur realm, open bool) {
137	assertAdmin(0, cur)
138	mintOpen = open
139	chain.Emit("MintOpen", "open", boolString(open))
140}
141
142// SetPrice sets what a mint costs, from 1 to 10000 GNOT. Admin only. It
143// applies to the next mint.
144func SetPrice(cur realm, ugnot int64) {
145	assertAdmin(0, cur)
146	if ugnot < minPrice || ugnot > maxPrice {
147		panic(errInvalidPrice)
148	}
149	price = ugnot
150	chain.Emit("PriceSet", "price", itoa(ugnot))
151}
152
153// SetMintCap sets how many planes one wallet may mint over its lifetime, from
154// 1 to 777. Admin only. A wallet already over a lowered cap keeps its planes
155// and mints no more.
156func SetMintCap(cur realm, n int64) {
157	assertAdmin(0, cur)
158	if n < 1 || n > supply {
159		panic(errInvalidCap)
160	}
161	mintCap = n
162	chain.Emit("MintCapSet", "cap", itoa(n))
163}
164
165// SetImageBase sets where the images and the metadata of the planes are
166// served from: the image of plane 42 is <base>042.png and its token URI is
167// <base>42.json. Admin only.
168//
169// It applies at once to every plane: the URLs are computed on read, nothing
170// is stored per token.
171func SetImageBase(cur realm, base string) {
172	assertAdmin(0, cur)
173	if !validBase(base) {
174		panic(errInvalidBase)
175	}
176	imageBase = base
177	chain.Emit("ImageBaseSet", "base", base)
178}
179
180// SetRoyaltyBps sets the royalty of a resale, from 0 to 1000 basis points,
181// and points it at the treasury of the game realm as it is now. Admin only.
182//
183// The royalty is advisory, as EIP-2981 is: a marketplace reads RoyaltyInfo
184// and pays it, the realm cannot enforce it on a transfer.
185func SetRoyaltyBps(cur realm, bps int64) {
186	assertAdmin(0, cur)
187	if bps < 0 || bps > maxRoyaltyBps {
188		panic(errInvalidRoyalty)
189	}
190	setRoyalty(bps)
191}
192
193// Minted returns how many planes were minted so far, burned ones included.
194// Planes are drawn at random, so it says nothing of which ids. The
195// settings (price, open, cap, image base, royalty) are read with
196// CollectionJSON: every exported function costs storage at deploy, measured
197// at about 650 bytes each, so there is no getter per setting.
198func Minted() int64 {
199	return mintCount
200}
201
202// webPathOf turns "gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes" into "/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/nfts/planes".
203func webPathOf(pkgPath string) string {
204	for i := 0; i < len(pkgPath); i++ {
205		if pkgPath[i] == '/' {
206			return pkgPath[i:]
207		}
208	}
209	return ""
210}
211
212// imageURL returns where the image of plane id is: <image base><id>.png,
213// the id on three digits, "042.png", as the image files are named. The token
214// URI keeps the plain id, "42.json".
215func imageURL(id int64) string {
216	return imageBase + pad3(id) + ".png"
217}
218
219// pad3 writes an id of 1 to 777 on three digits: 7 -> "007", 42 -> "042".
220func pad3(id int64) string {
221	s := itoa(id)
222	for len(s) < 3 {
223		s = "0" + s
224	}
225	return s
226}
227
228func itoa(n int64) string {
229	return strconv.FormatInt(n, 10)
230}
231
232func boolString(b bool) string {
233	if b {
234		return "true"
235	}
236	return "false"
237}