Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

gallery.gno

7.30 Kb · 208 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 is rendered as plain text (plainText: RA M-9,
 14// SA-M4): no heading, link, image, HTML or autolink can come out of a name,
 15// description, trait or manifest field. Every list is paginated.
 16package gallery
 17
 18import (
 19	"strconv"
 20	"strings"
 21
 22	"gno.land/p/nt/markdown/sanitize/v0"
 23	"gno.land/p/nt/ufmt/v0"
 24	"gno.land/r/sys/users"
 25
 26	"gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/types/v1"
 27	"gno.land/r/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/core"
 28	market "gno.land/r/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/market/v1"
 29)
 30
 31const (
 32	pageSize   = 20
 33	homeLimit  = 20
 34	maxPageNum = 500
 35)
 36
 37func Render(path string) string {
 38	route, query, _ := strings.Cut(path, "?")
 39	parts := strings.Split(route, "/")
 40	switch {
 41	case route == "":
 42		return home()
 43	case len(parts) == 2 && parts[0] == "c":
 44		return collectionPage(parts[1], pageFrom(query))
 45	case len(parts) == 4 && parts[0] == "c" && parts[2] == "t":
 46		return tokenPage(parts[1], parts[3])
 47	case len(parts) > 1 && parts[0] == "e":
 48		return enginePage(strings.TrimPrefix(route, "e/"))
 49	case len(parts) == 2 && parts[0] == "u":
 50		return userPage(parts[1])
 51	}
 52	return "# Not found\n\n[Back to the gallery](" + base() + ")\n"
 53}
 54
 55func home() string {
 56	n := core.CollectionCount()
 57	out := ufmt.Sprintf("# Gallery\n\n%d collections. Art is computed on chain from each collection's engine and revealed seed.\n\n", n)
 58	for id := n; id >= 1 && id > n-homeLimit; id-- {
 59		info := core.Info(itoa(id))
 60		out += ufmt.Sprintf("- [%s](%s) — %s, %d/%d minted\n",
 61			plainText(info.Name), link("c/"+info.ID), info.State.String(), info.Minted, info.MaxSupply)
 62	}
 63	return out
 64}
 65
 66func collectionPage(cid string, page int64) string {
 67	info := core.Info(cid)
 68	out := "# " + plainText(info.Name) + " (" + plainText(info.Symbol) + ")\n\n"
 69	if info.Description != "" {
 70		out += plainText(info.Description) + "\n\n"
 71	}
 72	out += ufmt.Sprintf("- State: **%s**, %d/%d minted, royalty %d bps\n", info.State.String(), info.Minted, info.MaxSupply, info.RoyaltyBps)
 73	out += "- Creator: " + who(info.Creator) + "\n"
 74	out += "- Engine: [" + plainText(info.EngineID) + "](" + link("e/"+info.EngineID) + ")\n"
 75	if info.Badge != "" {
 76		out += "- Provenance: **" + info.Badge + "** — " + fairness(cid) + "\n"
 77	}
 78	if f := market.Floor(cid); f > 0 {
 79		out += ufmt.Sprintf("- Floor: %d ugnot\n", f)
 80	}
 81	out += "\n## Tokens\n\n"
 82	first := (page-1)*pageSize + 1
 83	for n := first; n < first+pageSize && n <= info.Minted; n++ {
 84		tid := itoa(n)
 85		owner, err := core.OwnerOf(cid, tid)
 86		if err != nil {
 87			continue
 88		}
 89		line := "- [#" + tid + "](" + link("c/"+cid+"/t/"+tid) + ") owned by " + who(owner)
 90		if _, price, ok := market.ActiveListing(cid, tid); ok {
 91			line += ufmt.Sprintf(", listed at %d ugnot", price)
 92		}
 93		out += line + "\n"
 94	}
 95	if first+pageSize <= info.Minted {
 96		out += "\n[Next page](" + link(ufmt.Sprintf("c/%s?page=%d", cid, page+1)) + ")\n"
 97	}
 98	return out
 99}
100
101func tokenPage(cid, tid string) string {
102	info := core.Info(cid)
103	owner, err := core.OwnerOf(cid, tid)
104	if err != nil {
105		return "# Token not found\n"
106	}
107	uri, _ := core.TokenURI(cid, tid)
108	out := "# " + plainText(info.Name) + " #" + tid + "\n\n"
109	out += "![" + plainText(info.Name) + " #" + tid + "](" + sanitize.ImageURL(uri) + ")\n\n"
110	out += "- Owner: " + who(owner) + "\n"
111	if id, price, ok := market.ActiveListing(cid, tid); ok {
112		out += ufmt.Sprintf("- Listed: %d ugnot (listing %s)\n", price, id)
113	}
114	out += "\n## Traits\n\n"
115	for _, t := range core.TokenTraitList(cid, tid) {
116		out += "- " + plainText(t.Type) + ": " + plainText(t.Value) + "\n"
117	}
118	out += "\n## How to verify\n\n"
119	if info.State != types.StateFinal {
120		out += "The collection is not revealed yet; every token shows the same placeholder.\n"
121		return out
122	}
123	out += "1. Read `Provenance(\"" + cid + "\")` on core from another RPC and recompute the commitments and the salt.\n"
124	out += "2. Seed: `TokenSeed(\"" + cid + "\", \"" + tid + "\")` = sha256(\"" + "art|seed|v1|" + cid + "|" + tid + "|\" + salt).\n"
125	out += "3. Render it yourself: `RenderWith(engine, params, seed)` must return this image.\n\n"
126	out += "Provenance badge **" + info.Badge + "**, on-chain check: " + strconv.FormatBool(core.VerifyProvenance(cid)) +
127		"; " + fairness(cid) + "\n"
128	return out
129}
130
131// fairness never calls a collection "verified" unless it is a fair launch
132// (core.FairLaunch): a consistent record is not enough when one party could
133// choose the map (SA-M2).
134func fairness(cid string) string {
135	if core.FairLaunch(cid) {
136		return "fair launch, verified on chain"
137	}
138	if !core.VerifyProvenance(cid) {
139		return "NOT verified: the provenance record does not check out"
140	}
141	return "not a fair launch: one party could choose the map (record consistent)"
142}
143
144// plainText renders user text as inert markdown: sanitize.InlineText escapes
145// every inline-active character (\ * _ [ ] ( ) ~ > - + . ! ` # < &) and folds
146// newlines, and escaping ':' breaks bare-URL autolinks ("https\://…"), which
147// InlineText leaves alone (SA-M4).
148func plainText(s string) string {
149	return strings.ReplaceAll(sanitize.InlineText(s), ":", "\\:")
150}
151
152func enginePage(id string) string {
153	m := core.EngineManifest(id)
154	out := "# Engine " + plainText(m.Name) + " " + plainText(m.Version) + "\n\n"
155	out += "- Realm: " + sanitize.InlineCode(id) + "\n"
156	out += "- Author: " + who(m.Author) + "\n"
157	out += "- Status: **" + core.EngineStatus(id) + "**, sample fingerprint intact: " + strconv.FormatBool(core.VerifyEngine(id)) + "\n"
158	out += "- License: " + plainText(m.License) + "\n"
159	out += "- Traits: " + plainText(strings.Join(m.TraitTypes, ", ")) + "\n\n"
160	if m.Description != "" {
161		out += plainText(m.Description) + "\n"
162	}
163	return out
164}
165
166func userPage(name string) string {
167	u, _ := users.ResolveName(name)
168	if u == nil {
169		return "# Unknown user\n"
170	}
171	addr := u.Addr()
172	return "# @" + sanitize.InlineText(u.Name()) + "\n\n`" + addr.String() + "`\n\n" +
173		"Collections and listings of this address: reader realm, `Profile(" + addr.String() + ")`.\n"
174}
175
176// who renders an address as its r/sys/users name when it has one.
177func who(addr address) string {
178	if u := users.ResolveAddress(addr); u != nil {
179		return "[@" + sanitize.InlineText(u.Name()) + "](/u/" + sanitize.InlineText(u.Name()) + ")"
180	}
181	return "`" + addr.String() + "`"
182}
183
184func pageFrom(query string) int64 {
185	for _, kv := range strings.Split(query, "&") {
186		if strings.HasPrefix(kv, "page=") {
187			if n, err := strconv.ParseInt(strings.TrimPrefix(kv, "page="), 10, 64); err == nil && n >= 1 && n <= maxPageNum {
188				return n
189			}
190		}
191	}
192	return 1
193}
194
195// selfPath is captured at deploy; links are gnoweb paths without the domain.
196var selfPath string
197
198func init(cur realm) {
199	selfPath = cur.PkgPath()
200}
201
202func base() string {
203	return strings.TrimPrefix(selfPath, "gno.land")
204}
205
206func link(route string) string { return base() + ":" + route }
207
208func itoa(n int64) string { return strconv.FormatInt(n, 10) }