Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

9.09 Kb · 338 lines
  1package block
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/akkadia/v0/grc1155"
  9	"gno.land/p/jeronimoalbi/pager"
 10	"gno.land/r/akkadia/v0/admin"
 11)
 12
 13func Render(iUrl string) string {
 14	assertMigrationStateAvailable()
 15	u, err := url.Parse(iUrl)
 16	if err != nil {
 17		return "404\n"
 18	}
 19
 20	path := u.Path
 21	query := u.Query()
 22
 23	switch {
 24	case path == "":
 25		return renderHome()
 26	case path == "blocks":
 27		return renderBlocksList(iUrl)
 28	case path == "block":
 29		return renderBlock(query.Get("id"), query.Get("page"))
 30	case path == "inventory":
 31		return renderInventory(query.Get("owner"))
 32	case path == "grc1155":
 33		return renderGRC1155(query.Get("id"), query.Get("owner"))
 34	default:
 35		return "404\n"
 36	}
 37}
 38
 39func renderHome() string {
 40	output := `# Block
 41
 42Welcome to the Block management system on Akkadia.
 43
 44## What is Block?
 45
 46Block is a GRC1155-based multi-token system where each block type represents a unique item in the game:
 47
 48* **GRC1155 Token**: Each block type is a fungible token with supply limits
 49* **Mintable**: Users can mint blocks by paying the mint price
 50* **Tradeable**: Blocks can be transferred between users
 51* **Usable**: Blocks can be installed/uninstalled in chunks
 52
 53## How Blocks Work
 54
 551. **System Blocks**: Admin creates system blocks (ID < 100000)
 562. **User Blocks**: Users can create custom blocks (ID >= 100000)
 573. **Minting**: Pay mint price to receive block tokens
 584. **Usage**: Install blocks in chunks, uninstall to recover
 59
 60## Config
 61
 62`
 63	output += "* **List Limit**: " + strconv.Itoa(listLimit) + "\n"
 64	output += "* **Creator BPS**: " + strconv.Itoa(creatorBPS) + " (" + formatBPSPercentInt(creatorBPS) + ")\n"
 65
 66	output += `
 67## Stats
 68
 69`
 70	output += "* **Total Blocks**: " + strconv.Itoa(blockStore.Total()) + "\n"
 71	output += "* **Next Block ID**: " + uint32String(blockStore.NextID()) + "\n"
 72
 73	output += `
 74## Quick Links
 75
 76* [Browse All Blocks](./block:blocks)
 77
 78## Search Block by ID
 79
 80<gno-form path="block">
 81  <gno-input name="id" placeholder="Enter block ID" />
 82</gno-form>
 83
 84## Search Inventory by Owner
 85
 86<gno-form path="inventory">
 87  <gno-input name="owner" placeholder="Enter owner address" />
 88</gno-form>
 89
 90## Check GRC1155 Balance
 91
 92<gno-form path="grc1155">
 93  <gno-input name="id" placeholder="Enter block ID" />
 94  <gno-input name="owner" placeholder="Enter owner address" />
 95</gno-form>
 96`
 97
 98	return output
 99}
100
101// renderBlocksList renders a list of all blocks with pagination
102func renderBlocksList(iUrl string) string {
103	output := `# All Blocks
104
105[← Home](./block:)
106
107`
108	if blockStore.Total() == 0 {
109		output += "*No blocks created yet.*\n"
110		return output
111	}
112
113	pages, err := pager.New(iUrl, blockStore.Total(), pager.WithPageSize(10))
114	if err != nil {
115		return output + "Invalid page"
116	}
117
118	// Get current page number for back links
119	currentPage := (pages.Offset() / pages.PageSize()) + 1
120
121	for _, block := range blockStore.List((pages.Offset()/pages.PageSize())+1, pages.PageSize()) {
122		blockID := block["id"]
123		output += "### [" + escapeMarkdown(blockID+" - "+block["name"]) + "](./block:block?id=" + url.QueryEscape(blockID) + "&page=" + strconv.Itoa(currentPage) + ")\n\n"
124		output += "* **Type**: " + escapeMarkdown(block["blockType"]) + "\n"
125		output += "* **Max Supply**: " + escapeMarkdown(block["maxSupply"]) + "\n"
126		output += "* **Mint Price**: " + escapeMarkdown(block["mintPrice"]) + " ugnot\n"
127		output += "\n---\n\n"
128	}
129
130	if pages.HasPages() {
131		output += pager.Picker(pages)
132	}
133
134	return output
135}
136
137func renderBlock(blockID string, page string) string {
138	output := "# Block Detail\n\n"
139	if page != "" {
140		output += "[← Back to List](./block:blocks?page=" + url.QueryEscape(page) + ") | [View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/block?id=" + url.QueryEscape(blockID) + ")\n\n"
141	} else {
142		output += "[← Home](./block:) | [View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/block?id=" + url.QueryEscape(blockID) + ")\n\n"
143	}
144
145	bid, ok := parseBlockID(blockID)
146	if !ok {
147		return output + "Invalid block ID"
148	}
149	properties, found := blockStore.Get(bid)
150	if !found {
151		return output + "Block not found"
152	}
153
154	textureURL := properties["textureURL"]
155	previewURL := properties["previewURL"]
156
157	output += "## " + escapeMarkdown(properties["name"]) + "\n\n"
158
159	output += `## Images
160<gno-columns>
161### Texture
162
163|||
164
165### Preview
166</gno-columns>
167<gno-columns>
168`
169	output += renderMarkdownImage("TextureURL", textureURL, "") + "\n\n|||\n\n" + renderMarkdownImage("PreviewURL", previewURL, "192") + "\n"
170	output += "</gno-columns>\n\n"
171
172	output += "## Properties\n\n"
173
174	for key, value := range properties {
175		output += renderBlockProperty(key, value)
176	}
177
178	return output
179}
180
181func renderInventory(owner string) string {
182	output := "# Inventory of " + escapeMarkdown(owner) + "\n\n"
183	output += "[← Home](./block:)\n\n"
184
185	if owner == "" {
186		return output + "Owner address is required"
187	}
188
189	inventory := GetInventory(address(owner))
190
191	if len(inventory) == 0 {
192		output += "*No blocks found for this owner.*\n"
193		return output
194	}
195
196	output += "**Total Block Types**: " + strconv.Itoa(len(inventory)) + "\n\n"
197	output += "---\n\n"
198
199	for _, item := range inventory {
200		blockID := item["id"]
201		balance := item["balance"]
202
203		// Get block info
204		bid, ok := parseBlockID(blockID)
205		block, found := blockStore.Get(bid)
206
207		if ok && found {
208			output += "### [" + escapeMarkdown(blockID+" - "+block["name"]) + "](./block:block?id=" + url.QueryEscape(blockID) + ")\n\n"
209			output += "* **Balance**: " + escapeMarkdown(balance) + "\n"
210			output += "* **Type**: " + escapeMarkdown(block["blockType"]) + "\n"
211		} else {
212			output += "### " + escapeMarkdown(blockID) + "\n\n"
213			output += "* **Balance**: " + escapeMarkdown(balance) + "\n"
214		}
215		output += "\n---\n\n"
216	}
217
218	return output
219}
220
221func renderGRC1155(blockID string, owner string) string {
222	output := "# GRC1155 Balance\n\n"
223	output += "[← Home](./block:)\n\n"
224
225	if blockID == "" || owner == "" {
226		return output + "Block ID and owner address are required"
227	}
228
229	balance, found := balanceOfSafe(address(owner), grc1155.TokenID(blockID))
230	if !found {
231		return output + "Balance unavailable"
232	}
233
234	output += "* **Block ID**: " + escapeMarkdown(blockID) + "\n"
235	output += "* **Owner**: " + escapeMarkdown(owner) + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + url.QueryEscape(owner) + ")]\n"
236	output += "* **Balance**: " + strconv.FormatInt(balance, 10) + "\n\n"
237
238	return output
239}
240
241func uint32String(value uint32) string {
242	return strconv.FormatUint(uint64(value), 10)
243}
244
245func formatBPSPercent(value uint32) string {
246	return uint32String(value/100) + "." + pad2(value%100) + "%"
247}
248
249func formatBPSPercentInt(value int) string {
250	return strconv.Itoa(value/100) + "." + pad2Int(value%100) + "%"
251}
252
253func pad2(value uint32) string {
254	if value < 10 {
255		return "0" + uint32String(value)
256	}
257	return uint32String(value)
258}
259
260func pad2Int(value int) string {
261	if value < 10 {
262		return "0" + strconv.Itoa(value)
263	}
264	return strconv.Itoa(value)
265}
266
267func renderBlockProperty(key, value string) string {
268	escapedKey := escapeMarkdown(key)
269	escapedValue := escapeMarkdown(value)
270	if key == "mintPrice" || key == "usePrice" {
271		return "* **" + escapedKey + "**: " + escapedValue + " ugnot\n"
272	}
273	if key == "installerBps" {
274		bps, ok := parseBlockID(value)
275		if ok {
276			return "* **" + escapedKey + "**: " + escapedValue + " (" + formatBPSPercent(bps) + ")\n"
277		}
278		return "* **" + escapedKey + "**: " + escapedValue + "\n"
279	}
280	if key == "creator" {
281		return "* **" + escapedKey + "**: " + escapedValue + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + url.QueryEscape(value) + ")]\n"
282	}
283	if key == "textureURL" || key == "previewURL" {
284		if isSafeRenderURL(value) {
285			return "* **" + escapedKey + "**: [" + escapedValue + "](" + value + ")\n"
286		}
287		return "* **" + escapedKey + "**: " + escapedValue + "\n"
288	}
289	return "* **" + escapedKey + "**: " + escapedValue + "\n"
290}
291
292func renderMarkdownImage(label, value, size string) string {
293	if isSafeRenderURL(value) {
294		if size != "" {
295			return "![" + label + "](" + value + " " + size + ")"
296		}
297		return "![" + label + "](" + value + ")"
298	}
299	return "*" + label + "*: " + escapeMarkdown(value)
300}
301
302func isSafeRenderURL(value string) bool {
303	if !(strings.HasPrefix(value, "https://") || strings.HasPrefix(value, "http://")) {
304		return false
305	}
306	for i := 0; i < len(value); i++ {
307		switch value[i] {
308		case ' ', '\t', '\n', '\r', '(', ')', '[', ']', '<', '>', '\\':
309			return false
310		}
311		if value[i] < 0x20 || value[i] == 0x7f {
312			return false
313		}
314	}
315	return true
316}
317
318func escapeMarkdown(s string) string {
319	s = strings.ReplaceAll(s, "\\", "\\\\")
320	s = strings.ReplaceAll(s, "\n", "\\n")
321	s = strings.ReplaceAll(s, "`", "\\`")
322	s = strings.ReplaceAll(s, "*", "\\*")
323	s = strings.ReplaceAll(s, "_", "\\_")
324	s = strings.ReplaceAll(s, "{", "\\{")
325	s = strings.ReplaceAll(s, "}", "\\}")
326	s = strings.ReplaceAll(s, "[", "\\[")
327	s = strings.ReplaceAll(s, "]", "\\]")
328	s = strings.ReplaceAll(s, "(", "\\(")
329	s = strings.ReplaceAll(s, ")", "\\)")
330	s = strings.ReplaceAll(s, "#", "\\#")
331	s = strings.ReplaceAll(s, "+", "\\+")
332	s = strings.ReplaceAll(s, "-", "\\-")
333	s = strings.ReplaceAll(s, ".", "\\.")
334	s = strings.ReplaceAll(s, "!", "\\!")
335	s = strings.ReplaceAll(s, "|", "\\|")
336	s = strings.ReplaceAll(s, ">", "\\>")
337	return s
338}