package block
import (
"net/url"
"strconv"
"strings"
"gno.land/p/akkadia/v0/grc1155"
"gno.land/p/jeronimoalbi/pager"
"gno.land/r/akkadia/v0/admin"
)
func Render(iUrl string) string {
assertMigrationStateAvailable()
u, err := url.Parse(iUrl)
if err != nil {
return "404\n"
}
path := u.Path
query := u.Query()
switch {
case path == "":
return renderHome()
case path == "blocks":
return renderBlocksList(iUrl)
case path == "block":
return renderBlock(query.Get("id"), query.Get("page"))
case path == "inventory":
return renderInventory(query.Get("owner"))
case path == "grc1155":
return renderGRC1155(query.Get("id"), query.Get("owner"))
default:
return "404\n"
}
}
func renderHome() string {
output := `# Block
Welcome to the Block management system on Akkadia.
## What is Block?
Block is a GRC1155-based multi-token system where each block type represents a unique item in the game:
* **GRC1155 Token**: Each block type is a fungible token with supply limits
* **Mintable**: Users can mint blocks by paying the mint price
* **Tradeable**: Blocks can be transferred between users
* **Usable**: Blocks can be installed/uninstalled in chunks
## How Blocks Work
1. **System Blocks**: Admin creates system blocks (ID < 100000)
2. **User Blocks**: Users can create custom blocks (ID >= 100000)
3. **Minting**: Pay mint price to receive block tokens
4. **Usage**: Install blocks in chunks, uninstall to recover
## Config
`
output += "* **List Limit**: " + strconv.Itoa(listLimit) + "\n"
output += "* **Creator BPS**: " + strconv.Itoa(creatorBPS) + " (" + formatBPSPercentInt(creatorBPS) + ")\n"
output += `
## Stats
`
output += "* **Total Blocks**: " + strconv.Itoa(blockStore.Total()) + "\n"
output += "* **Next Block ID**: " + uint32String(blockStore.NextID()) + "\n"
output += `
## Quick Links
* [Browse All Blocks](./block:blocks)
## Search Block by ID
## Search Inventory by Owner
## Check GRC1155 Balance
`
return output
}
// renderBlocksList renders a list of all blocks with pagination
func renderBlocksList(iUrl string) string {
output := `# All Blocks
[← Home](./block:)
`
if blockStore.Total() == 0 {
output += "*No blocks created yet.*\n"
return output
}
pages, err := pager.New(iUrl, blockStore.Total(), pager.WithPageSize(10))
if err != nil {
return output + "Invalid page"
}
// Get current page number for back links
currentPage := (pages.Offset() / pages.PageSize()) + 1
for _, block := range blockStore.List((pages.Offset()/pages.PageSize())+1, pages.PageSize()) {
blockID := block["id"]
output += "### [" + escapeMarkdown(blockID+" - "+block["name"]) + "](./block:block?id=" + url.QueryEscape(blockID) + "&page=" + strconv.Itoa(currentPage) + ")\n\n"
output += "* **Type**: " + escapeMarkdown(block["blockType"]) + "\n"
output += "* **Max Supply**: " + escapeMarkdown(block["maxSupply"]) + "\n"
output += "* **Mint Price**: " + escapeMarkdown(block["mintPrice"]) + " ugnot\n"
output += "\n---\n\n"
}
if pages.HasPages() {
output += pager.Picker(pages)
}
return output
}
func renderBlock(blockID string, page string) string {
output := "# Block Detail\n\n"
if page != "" {
output += "[← Back to List](./block:blocks?page=" + url.QueryEscape(page) + ") | [View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/block?id=" + url.QueryEscape(blockID) + ")\n\n"
} else {
output += "[← Home](./block:) | [View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/block?id=" + url.QueryEscape(blockID) + ")\n\n"
}
bid, ok := parseBlockID(blockID)
if !ok {
return output + "Invalid block ID"
}
properties, found := blockStore.Get(bid)
if !found {
return output + "Block not found"
}
textureURL := properties["textureURL"]
previewURL := properties["previewURL"]
output += "## " + escapeMarkdown(properties["name"]) + "\n\n"
output += `## Images
### Texture
|||
### Preview
`
output += renderMarkdownImage("TextureURL", textureURL, "") + "\n\n|||\n\n" + renderMarkdownImage("PreviewURL", previewURL, "192") + "\n"
output += "\n\n"
output += "## Properties\n\n"
for key, value := range properties {
output += renderBlockProperty(key, value)
}
return output
}
func renderInventory(owner string) string {
output := "# Inventory of " + escapeMarkdown(owner) + "\n\n"
output += "[← Home](./block:)\n\n"
if owner == "" {
return output + "Owner address is required"
}
inventory := GetInventory(address(owner))
if len(inventory) == 0 {
output += "*No blocks found for this owner.*\n"
return output
}
output += "**Total Block Types**: " + strconv.Itoa(len(inventory)) + "\n\n"
output += "---\n\n"
for _, item := range inventory {
blockID := item["id"]
balance := item["balance"]
// Get block info
bid, ok := parseBlockID(blockID)
block, found := blockStore.Get(bid)
if ok && found {
output += "### [" + escapeMarkdown(blockID+" - "+block["name"]) + "](./block:block?id=" + url.QueryEscape(blockID) + ")\n\n"
output += "* **Balance**: " + escapeMarkdown(balance) + "\n"
output += "* **Type**: " + escapeMarkdown(block["blockType"]) + "\n"
} else {
output += "### " + escapeMarkdown(blockID) + "\n\n"
output += "* **Balance**: " + escapeMarkdown(balance) + "\n"
}
output += "\n---\n\n"
}
return output
}
func renderGRC1155(blockID string, owner string) string {
output := "# GRC1155 Balance\n\n"
output += "[← Home](./block:)\n\n"
if blockID == "" || owner == "" {
return output + "Block ID and owner address are required"
}
balance, found := balanceOfSafe(address(owner), grc1155.TokenID(blockID))
if !found {
return output + "Balance unavailable"
}
output += "* **Block ID**: " + escapeMarkdown(blockID) + "\n"
output += "* **Owner**: " + escapeMarkdown(owner) + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + url.QueryEscape(owner) + ")]\n"
output += "* **Balance**: " + strconv.FormatInt(balance, 10) + "\n\n"
return output
}
func uint32String(value uint32) string {
return strconv.FormatUint(uint64(value), 10)
}
func formatBPSPercent(value uint32) string {
return uint32String(value/100) + "." + pad2(value%100) + "%"
}
func formatBPSPercentInt(value int) string {
return strconv.Itoa(value/100) + "." + pad2Int(value%100) + "%"
}
func pad2(value uint32) string {
if value < 10 {
return "0" + uint32String(value)
}
return uint32String(value)
}
func pad2Int(value int) string {
if value < 10 {
return "0" + strconv.Itoa(value)
}
return strconv.Itoa(value)
}
func renderBlockProperty(key, value string) string {
escapedKey := escapeMarkdown(key)
escapedValue := escapeMarkdown(value)
if key == "mintPrice" || key == "usePrice" {
return "* **" + escapedKey + "**: " + escapedValue + " ugnot\n"
}
if key == "installerBps" {
bps, ok := parseBlockID(value)
if ok {
return "* **" + escapedKey + "**: " + escapedValue + " (" + formatBPSPercent(bps) + ")\n"
}
return "* **" + escapedKey + "**: " + escapedValue + "\n"
}
if key == "creator" {
return "* **" + escapedKey + "**: " + escapedValue + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + url.QueryEscape(value) + ")]\n"
}
if key == "textureURL" || key == "previewURL" {
if isSafeRenderURL(value) {
return "* **" + escapedKey + "**: [" + escapedValue + "](" + value + ")\n"
}
return "* **" + escapedKey + "**: " + escapedValue + "\n"
}
return "* **" + escapedKey + "**: " + escapedValue + "\n"
}
func renderMarkdownImage(label, value, size string) string {
if isSafeRenderURL(value) {
if size != "" {
return ""
}
return ""
}
return "*" + label + "*: " + escapeMarkdown(value)
}
func isSafeRenderURL(value string) bool {
if !(strings.HasPrefix(value, "https://") || strings.HasPrefix(value, "http://")) {
return false
}
for i := 0; i < len(value); i++ {
switch value[i] {
case ' ', '\t', '\n', '\r', '(', ')', '[', ']', '<', '>', '\\':
return false
}
if value[i] < 0x20 || value[i] == 0x7f {
return false
}
}
return true
}
func escapeMarkdown(s string) string {
s = strings.ReplaceAll(s, "\\", "\\\\")
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "`", "\\`")
s = strings.ReplaceAll(s, "*", "\\*")
s = strings.ReplaceAll(s, "_", "\\_")
s = strings.ReplaceAll(s, "{", "\\{")
s = strings.ReplaceAll(s, "}", "\\}")
s = strings.ReplaceAll(s, "[", "\\[")
s = strings.ReplaceAll(s, "]", "\\]")
s = strings.ReplaceAll(s, "(", "\\(")
s = strings.ReplaceAll(s, ")", "\\)")
s = strings.ReplaceAll(s, "#", "\\#")
s = strings.ReplaceAll(s, "+", "\\+")
s = strings.ReplaceAll(s, "-", "\\-")
s = strings.ReplaceAll(s, ".", "\\.")
s = strings.ReplaceAll(s, "!", "\\!")
s = strings.ReplaceAll(s, "|", "\\|")
s = strings.ReplaceAll(s, ">", "\\>")
return s
}