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}