reader.gno
5.39 Kb · 201 lines
1// Package reader is the batch JSON read API for the frontend and the
2// aggregator (design §2.5.6). It is private (re-deployable in place, no
3// other realm may import it) and holds no state or money: everything comes
4// from core, market and r/sys/users.
5//
6// Every string goes out through safe.JSONWriter, so stored text can never
7// forge a record (RA H-1: the old TSV reader split on tabs and newlines).
8// Art is never batched: one token per Art call, because SVG rendering is
9// what exhausted the query gas in nftread1 (RA L-9).
10package reader
11
12import (
13 "gno.land/r/sys/users"
14
15 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/engine/v1"
16 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/safe/v1"
17 "gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/core"
18 market "gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/market/v1"
19)
20
21// Batch limits. Hypothesis (S-4): sized for the genesis engine under the
22// public qeval gas cap; measure on Onyx and lower if a full page runs out.
23const (
24 MaxCards = 50
25 MaxTraitCounts = 500
26 MaxStudio = 200
27)
28
29// Cards is one gallery page: [{tid, owner, ownerName, traits, listing}] for
30// tokens offset+1 … offset+limit. Burned tokens are skipped; listing is
31// {id, price} or null.
32func Cards(cid string, offset, limit int64) string {
33 info := core.Info(cid)
34 w := &safe.JSONWriter{}
35 w.BeginArray()
36 for _, tid := range tokenRange(info.Minted, offset, limit, MaxCards) {
37 owner, err := core.OwnerOf(cid, tid)
38 if err != nil {
39 continue // burned
40 }
41 w.BeginObject()
42 w.StrField("tid", tid)
43 w.StrField("owner", owner.String())
44 w.StrField("ownerName", nameOf(owner))
45 w.Key("traits")
46 writeTraits(w, core.TokenTraitList(cid, tid))
47 w.Key("listing")
48 if id, price, ok := market.ActiveListing(cid, tid); ok {
49 w.BeginObject()
50 w.StrField("id", id)
51 w.IntField("price", price)
52 w.EndObject()
53 } else {
54 w.Raw("null")
55 }
56 w.EndObject()
57 }
58 w.EndArray()
59 return w.String()
60}
61
62// TraitCounts counts trait values over tokens offset+1 … offset+limit:
63// {"n": tokens counted, "counts": {type: {value: count}}}. Rarity % for a
64// 1000-token collection takes two calls. Before Final every token reports
65// Status=Unrevealed.
66func TraitCounts(cid string, offset, limit int64) string {
67 info := core.Info(cid)
68 c := newCounter()
69 for _, tid := range tokenRange(info.Minted, offset, limit, MaxTraitCounts) {
70 if _, err := core.OwnerOf(cid, tid); err != nil {
71 continue
72 }
73 c.add(core.TokenTraitList(cid, tid))
74 }
75 return c.json()
76}
77
78// StudioTraits is TraitCounts for an engine and params over n sample seeds
79// (engine.SampleSeed(from) …), for the Studio's rarity preview.
80func StudioTraits(engineID, params string, from, n int64) string {
81 if n <= 0 || n > MaxStudio {
82 n = MaxStudio
83 }
84 if from < 0 {
85 from = 0
86 }
87 c := newCounter()
88 for i := from; i < from+n; i++ {
89 c.add(core.EngineTraits(engineID, params, engine.SampleSeed(int(i))))
90 }
91 return c.json()
92}
93
94// Art is one token's data URI (placeholder before Final).
95func Art(cid, tid string) string {
96 uri, err := core.TokenURI(cid, tid)
97 if err != nil {
98 panic(err)
99 }
100 return uri
101}
102
103func Collections(offset, limit int64) string { return core.Collections(offset, limit) }
104
105// Profile is {address, name, collections, listings} for one address.
106func Profile(addr address) string {
107 w := &safe.JSONWriter{}
108 w.BeginObject()
109 w.StrField("address", addr.String())
110 w.StrField("name", nameOf(addr))
111 w.Key("collections")
112 w.Raw(core.CollectionsByCreator(addr, 0, MaxCards))
113 w.Key("listings")
114 w.Raw(market.ListingsBySeller(addr, 0, MaxCards))
115 w.EndObject()
116 return w.String()
117}
118
119// --- helpers -----------------------------------------------------------------
120
121// nameOf is the r/sys/users name of addr, or "".
122func nameOf(addr address) string {
123 if u := users.ResolveAddress(addr); u != nil {
124 return u.Name()
125 }
126 return ""
127}
128
129func tokenRange(minted, offset, limit, max int64) []string {
130 if limit <= 0 || limit > max {
131 limit = max
132 }
133 if offset < 0 {
134 offset = 0
135 }
136 var out []string
137 for n := offset + 1; n <= minted && n <= offset+limit; n++ {
138 out = append(out, itoa(n))
139 }
140 return out
141}
142
143func writeTraits(w *safe.JSONWriter, ts []engine.Trait) {
144 w.BeginArray()
145 for _, t := range ts {
146 w.BeginObject()
147 w.StrField("trait_type", t.Type)
148 w.StrField("value", t.Value)
149 w.EndObject()
150 }
151 w.EndArray()
152}
153
154// counter keeps first-seen order so the JSON is deterministic.
155type counter struct {
156 n int64
157 types []string
158 values map[string][]string // type -> values in first-seen order
159 counts map[string]map[string]int64 // type -> value -> count
160}
161
162func newCounter() *counter {
163 return &counter{values: map[string][]string{}, counts: map[string]map[string]int64{}}
164}
165
166func (c *counter) add(ts []engine.Trait) {
167 c.n++
168 for _, t := range ts {
169 byValue, ok := c.counts[t.Type]
170 if !ok {
171 byValue = map[string]int64{}
172 c.counts[t.Type] = byValue
173 c.types = append(c.types, t.Type)
174 }
175 if _, seen := byValue[t.Value]; !seen {
176 c.values[t.Type] = append(c.values[t.Type], t.Value)
177 }
178 byValue[t.Value]++
179 }
180}
181
182func (c *counter) json() string {
183 w := &safe.JSONWriter{}
184 w.BeginObject()
185 w.IntField("n", c.n)
186 w.Key("counts")
187 w.BeginObject()
188 for _, typ := range c.types {
189 w.Key(typ)
190 w.BeginObject()
191 for _, v := range c.values[typ] {
192 w.IntField(v, c.counts[typ][v])
193 }
194 w.EndObject()
195 }
196 w.EndObject()
197 w.EndObject()
198 return w.String()
199}
200
201func itoa(n int64) string { return safe.PadInt(n, 1) }