json.gno
5.09 Kb · 148 lines
1package planes
2
3import (
4 "strings"
5
6 "gno.land/p/onbloc/json/v0"
7)
8
9// The JSON documents are built with gno.land/p/onbloc/json/v0, which quotes
10// every string. Keys come out in the order they are written: a gno map
11// iterates in insertion order, and the library keeps an object's members in
12// one. Numbers are float64 in the library and printed without exponent or
13// decimal point when whole, which every number here is, far under 2^53.
14
15// PlaneJSON returns one plane as a JSON object, minted or not:
16//
17// { "id": 42, "name": "Storm Saint", "model": "delta", "pilot": "gnome",
18// "body": "#1e1b4b", "wing": "#1e293b", "accent": "#00f5ff", "tier": "epic",
19// "laser": "#39ff14", "image": "https://gnofly.xyz/nft/collections/planes/042.png", "owner": "g1..." }
20//
21// The output has no whitespace. "owner" is "" for a plane not minted, or
22// burned. An id outside 1 to 777 returns "". Which plane a pilot flies is
23// not on-chain: the game server checks the owner when a pilot picks one.
24func PlaneJSON(id int64) string {
25 p, ok := getPlane(id)
26 if !ok {
27 return ""
28 }
29 b := json.Builder()
30 writePlane(b, p)
31 b.WriteString("image", imageURL(id))
32 b.WriteString("owner", ownerOf(id).String())
33 return encode(b.Node())
34}
35
36// PlanesOf returns the ids of the planes addr owns as a JSON array, in
37// ascending order: [3,42,700]. It is [] for an address without planes.
38func PlanesOf(addr address) string {
39 ids := []*json.Node{}
40 if n, err := Token.BalanceOf(addr); err == nil && n > 0 {
41 // Ids are dense, 1 to 777: a scan that stops at the last plane of
42 // addr is cheaper than keeping a list per owner up to date at every
43 // mint and transfer.
44 for id := int64(1); id <= supply && int64(len(ids)) < n; id++ {
45 if ownerOf(id) == addr {
46 ids = append(ids, json.NumberNode("", float64(id)))
47 }
48 }
49 }
50 return encode(json.ArrayNode("", ids))
51}
52
53// maxMintedPage is the most ids MintedJSON looks at in one call.
54const maxMintedPage = int64(200)
55
56// MintedJSON returns who owns the minted planes among the ids offset+1 to
57// offset+limit, as a JSON array in id order:
58//
59// [{"id":3,"owner":"g1..."},{"id":42,"owner":""},...]
60//
61// Ids not minted are left out; "owner" is "" for a burned plane. limit is
62// capped at 200, so four calls (offsets 0, 200, 400, 600) cover the 777,
63// whatever was minted: planes are drawn at random, minted ids are not a
64// prefix. It is what the website's grid reads. An empty range is [].
65func MintedJSON(offset, limit int64) string {
66 if limit > maxMintedPage {
67 limit = maxMintedPage
68 }
69 entries := []*json.Node{}
70 if offset >= 0 && limit > 0 {
71 last := offset + limit
72 if last > supply {
73 last = supply
74 }
75 for id := offset + 1; id <= last; id++ {
76 if !wasMinted(id) {
77 continue
78 }
79 entry := json.Builder().
80 WriteNumber("id", float64(id)).
81 WriteString("owner", ownerOf(id).String())
82 entries = append(entries, entry.Node())
83 }
84 }
85 return encode(json.ArrayNode("", entries))
86}
87
88// CollectionJSON returns the state of the collection as one JSON object:
89//
90// { "name": "gnofly planes", "symbol": "PLANE",
91// "supply": 777, "minted": 12, "burned": 0,
92// "price": 77000000, "open": true, "cap": 7,
93// "royaltyBps": 500, "treasury": "g1...",
94// "imageBase": "https://gnofly.xyz/nft/collections/planes/",
95// "tiers": { "common": { "supply": 500, "minted": 8 },
96// "rare": { "supply": 200, "minted": 3 },
97// "epic": { "supply": 70, "minted": 1 },
98// "legendary": { "supply": 7, "minted": 0 } } }
99//
100// The output has no whitespace. "minted" counts every mint so far, burned
101// planes included; "minted" minus "burned" is what exists.
102func CollectionJSON() string {
103 b := json.Builder().
104 WriteString("name", tokenName).
105 WriteString("symbol", tokenSymbol).
106 WriteNumber("supply", float64(supply)).
107 WriteNumber("minted", float64(Minted())).
108 WriteNumber("burned", float64(Minted()-Token.TotalSupply())).
109 WriteNumber("price", float64(price)).
110 WriteBool("open", mintOpen).
111 WriteNumber("cap", float64(mintCap)).
112 WriteNumber("royaltyBps", float64(RoyaltyBps())).
113 WriteString("treasury", roles.Treasury().String()).
114 WriteString("imageBase", imageBase).
115 WriteObject("tiers", func(tb *json.NodeBuilder) {
116 for i, tier := range strings.Split(tiers, ",") {
117 tb.WriteObject(tier, func(t *json.NodeBuilder) {
118 t.WriteNumber("supply", float64(tierSupply[i]))
119 t.WriteNumber("minted", float64(tierMinted[i]))
120 })
121 }
122 })
123 return encode(b.Node())
124}
125
126// writePlane writes the nine fields of a plane, in the order of a row, so
127// that a caller can add fields after them.
128func writePlane(b *json.NodeBuilder, p plane) {
129 b.WriteNumber("id", float64(p.id)).
130 WriteString("name", p.name).
131 WriteString("model", p.model).
132 WriteString("pilot", p.pilot).
133 WriteString("body", p.body).
134 WriteString("wing", p.wing).
135 WriteString("accent", p.accent).
136 WriteString("tier", p.tier).
137 WriteString("laser", p.laser)
138}
139
140// encode marshals a document built above. It cannot fail on a document
141// built from the builder, so a failure is a bug and panics.
142func encode(n *json.Node) string {
143 out, err := json.Marshal(n)
144 if err != nil {
145 panic(err)
146 }
147 return string(out)
148}