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

6.50 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/minhwa/types/v1"
 26	"gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/core"
 27	market "gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/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 += "![" + sanitize.InlineText(info.Name) + " #" + tid + "](" + sanitize.ImageURL(uri) + ")\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) }