gallery.gno
6.51 Kb · 185 lines
1// Package gallery is the platform's gnoweb face (design §2.5.7): the
2// product stays browsable on gno.land even without the website. It is
3// private (re-deployable in place), read-only and holds nothing.
4//
5// Routes:
6//
7// "" latest collections
8// c/<cid>[?page=N] one collection, 20 tokens per page
9// c/<cid>/t/<tid> one token: art, traits, owner, listing, provenance
10// e/<engine pkgpath> an engine's manifest and status
11// u/<name> a registered user's collections and listings
12//
13// Every user-supplied string goes through p/nt/markdown/sanitize (RA M-9);
14// every list is paginated.
15package gallery
16
17import (
18 "strconv"
19 "strings"
20
21 "gno.land/p/nt/markdown/sanitize/v0"
22 "gno.land/p/nt/ufmt/v0"
23 "gno.land/r/sys/users"
24
25 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/types/v1"
26 "gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/core"
27 market "gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/sinopia/market/v1"
28)
29
30const (
31 pageSize = 20
32 homeLimit = 20
33 maxPageNum = 500
34)
35
36func Render(path string) string {
37 route, query, _ := strings.Cut(path, "?")
38 parts := strings.Split(route, "/")
39 switch {
40 case route == "":
41 return home()
42 case len(parts) == 2 && parts[0] == "c":
43 return collectionPage(parts[1], pageFrom(query))
44 case len(parts) == 4 && parts[0] == "c" && parts[2] == "t":
45 return tokenPage(parts[1], parts[3])
46 case len(parts) > 1 && parts[0] == "e":
47 return enginePage(strings.TrimPrefix(route, "e/"))
48 case len(parts) == 2 && parts[0] == "u":
49 return userPage(parts[1])
50 }
51 return "# Not found\n\n[Back to the gallery](" + base() + ")\n"
52}
53
54func home() string {
55 n := core.CollectionCount()
56 out := ufmt.Sprintf("# Gallery\n\n%d collections. Art is computed on chain from each collection's engine and revealed seed.\n\n", n)
57 for id := n; id >= 1 && id > n-homeLimit; id-- {
58 info := core.Info(itoa(id))
59 out += ufmt.Sprintf("- [%s](%s) — %s, %d/%d minted\n",
60 sanitize.InlineText(info.Name), link("c/"+info.ID), info.State.String(), info.Minted, info.MaxSupply)
61 }
62 return out
63}
64
65func collectionPage(cid string, page int64) string {
66 info := core.Info(cid)
67 out := "# " + sanitize.InlineText(info.Name) + " (" + sanitize.InlineText(info.Symbol) + ")\n\n"
68 if info.Description != "" {
69 out += sanitize.Block(info.Description) + "\n\n"
70 }
71 out += ufmt.Sprintf("- State: **%s**, %d/%d minted, royalty %d bps\n", info.State.String(), info.Minted, info.MaxSupply, info.RoyaltyBps)
72 out += "- Creator: " + who(info.Creator) + "\n"
73 out += "- Engine: [" + sanitize.InlineText(info.EngineID) + "](" + link("e/"+info.EngineID) + ")\n"
74 if info.Badge != "" {
75 out += "- Provenance: **" + info.Badge + "**, verified: " + strconv.FormatBool(core.VerifyProvenance(cid)) + "\n"
76 }
77 if f := market.Floor(cid); f > 0 {
78 out += ufmt.Sprintf("- Floor: %d ugnot\n", f)
79 }
80 out += "\n## Tokens\n\n"
81 first := (page-1)*pageSize + 1
82 for n := first; n < first+pageSize && n <= info.Minted; n++ {
83 tid := itoa(n)
84 owner, err := core.OwnerOf(cid, tid)
85 if err != nil {
86 continue
87 }
88 line := "- [#" + tid + "](" + link("c/"+cid+"/t/"+tid) + ") owned by " + who(owner)
89 if _, price, ok := market.ActiveListing(cid, tid); ok {
90 line += ufmt.Sprintf(", listed at %d ugnot", price)
91 }
92 out += line + "\n"
93 }
94 if first+pageSize <= info.Minted {
95 out += "\n[Next page](" + link(ufmt.Sprintf("c/%s?page=%d", cid, page+1)) + ")\n"
96 }
97 return out
98}
99
100func tokenPage(cid, tid string) string {
101 info := core.Info(cid)
102 owner, err := core.OwnerOf(cid, tid)
103 if err != nil {
104 return "# Token not found\n"
105 }
106 uri, _ := core.TokenURI(cid, tid)
107 out := "# " + sanitize.InlineText(info.Name) + " #" + tid + "\n\n"
108 out += " + ")\n\n"
109 out += "- Owner: " + who(owner) + "\n"
110 if id, price, ok := market.ActiveListing(cid, tid); ok {
111 out += ufmt.Sprintf("- Listed: %d ugnot (listing %s)\n", price, id)
112 }
113 out += "\n## Traits\n\n"
114 for _, t := range core.TokenTraitList(cid, tid) {
115 out += "- " + sanitize.InlineText(t.Type) + ": " + sanitize.InlineText(t.Value) + "\n"
116 }
117 out += "\n## How to verify\n\n"
118 if info.State != types.StateFinal {
119 out += "The collection is not revealed yet; every token shows the same placeholder.\n"
120 return out
121 }
122 out += "1. Read `Provenance(\"" + cid + "\")` on core from another RPC and recompute the commitments and the salt.\n"
123 out += "2. Seed: `TokenSeed(\"" + cid + "\", \"" + tid + "\")` = sha256(\"" + "art|seed|v1|" + cid + "|" + tid + "|\" + salt).\n"
124 out += "3. Render it yourself: `RenderWith(engine, params, seed)` must return this image.\n\n"
125 out += "Provenance badge **" + info.Badge + "**, on-chain check: " + strconv.FormatBool(core.VerifyProvenance(cid)) + "\n"
126 return out
127}
128
129func enginePage(id string) string {
130 m := core.EngineManifest(id)
131 out := "# Engine " + sanitize.InlineText(m.Name) + " " + sanitize.InlineText(m.Version) + "\n\n"
132 out += "- Realm: `" + sanitize.InlineText(id) + "`\n"
133 out += "- Author: " + who(m.Author) + "\n"
134 out += "- Status: **" + core.EngineStatus(id) + "**, sample fingerprint intact: " + strconv.FormatBool(core.VerifyEngine(id)) + "\n"
135 out += "- License: " + sanitize.InlineText(m.License) + "\n"
136 out += "- Traits: " + sanitize.InlineText(strings.Join(m.TraitTypes, ", ")) + "\n\n"
137 if m.Description != "" {
138 out += sanitize.Block(m.Description) + "\n"
139 }
140 return out
141}
142
143func userPage(name string) string {
144 u, _ := users.ResolveName(name)
145 if u == nil {
146 return "# Unknown user\n"
147 }
148 addr := u.Addr()
149 return "# @" + sanitize.InlineText(u.Name()) + "\n\n`" + addr.String() + "`\n\n" +
150 "Collections and listings of this address: reader realm, `Profile(" + addr.String() + ")`.\n"
151}
152
153// who renders an address as its r/sys/users name when it has one.
154func who(addr address) string {
155 if u := users.ResolveAddress(addr); u != nil {
156 return "[@" + sanitize.InlineText(u.Name()) + "](/u/" + sanitize.InlineText(u.Name()) + ")"
157 }
158 return "`" + addr.String() + "`"
159}
160
161func pageFrom(query string) int64 {
162 for _, kv := range strings.Split(query, "&") {
163 if strings.HasPrefix(kv, "page=") {
164 if n, err := strconv.ParseInt(strings.TrimPrefix(kv, "page="), 10, 64); err == nil && n >= 1 && n <= maxPageNum {
165 return n
166 }
167 }
168 }
169 return 1
170}
171
172// selfPath is captured at deploy; links are gnoweb paths without the domain.
173var selfPath string
174
175func init(cur realm) {
176 selfPath = cur.PkgPath()
177}
178
179func base() string {
180 return strings.TrimPrefix(selfPath, "gno.land")
181}
182
183func link(route string) string { return base() + ":" + route }
184
185func itoa(n int64) string { return strconv.FormatInt(n, 10) }