render.gno
9.18 Kb · 338 lines
1package block
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7
8 "gno.land/p/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/grc1155"
9 "gno.land/p/jeronimoalbi/pager"
10 "gno.land/r/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/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 ""
296 }
297 return ""
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}