Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

6.51 Kb · 266 lines
  1package acr
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/r/akkadia/v0/admin"
  9)
 10
 11const pageSize = 20
 12
 13// Render handles RESTful routing and returns Markdown responses
 14func Render(iUrl string) string {
 15	assertMigrationStateAvailable()
 16	u, err := url.Parse(iUrl)
 17	if err != nil {
 18		return renderError("Invalid URL")
 19	}
 20
 21	query := u.Query()
 22
 23	switch u.Path {
 24	case "":
 25		return renderHome()
 26	case "top/balance":
 27		return renderTopByBalance(iUrl)
 28	case "top/minted":
 29		return renderTopByMinted(iUrl)
 30	case "hof":
 31		return renderHof()
 32	case "account":
 33		addr := query.Get("address")
 34		if addr == "" {
 35			return renderError("Address required")
 36		}
 37		return renderAccount(addr)
 38	default:
 39		// hof/{categoryName} routing
 40		if len(u.Path) > 4 && u.Path[:4] == "hof/" {
 41			name, err := url.PathUnescape(u.Path[4:])
 42			if err != nil {
 43				return renderError("Invalid category name")
 44			}
 45			return renderHofCategory(name)
 46		}
 47		return renderError("Page not found")
 48	}
 49}
 50
 51// renderHome renders the home page with stats, description, and links
 52func renderHome() string {
 53	output := "# " + acrStore.GetName() + " (" + acrStore.GetSymbol() + ")\n\n"
 54
 55	output += `## Introduction
 56
 57**Akkadia Community Rune (ACR)** is the utility token of the Akkadia ecosystem.
 58Users earn ACR through in-game activities and community participation.
 59
 60`
 61	output += "## Token Info\n\n"
 62	output += "* **Decimals**: " + strconv.Itoa(int(acrStore.GetDecimals())) + "\n"
 63	output += "* **Total Supply**: " + formatAmount(acrStore.TotalSupply()) + "\n"
 64	output += "* **Known Accounts**: " + strconv.Itoa(acrStore.KnownAccounts()) + "\n"
 65
 66	output += `
 67## Quick Links
 68
 69* [Top Holders by Balance](./acr:top/balance)
 70* [Top Holders by Minted](./acr:top/minted)
 71* [Hall of Fame](./acr:hof)
 72
 73## Account Lookup
 74
 75<gno-form path="account">
 76  <gno-input name="address" placeholder="Enter wallet address (g1...)" />
 77</gno-form>
 78`
 79	return output
 80}
 81
 82// renderTopByBalance renders paginated list of top holders by balance
 83func renderTopByBalance(iUrl string) string {
 84	output := "# Top Holders by Balance\n\n"
 85	output += "[← Home](../acr:)\n\n"
 86
 87	totalAccounts := acrStore.KnownAccounts()
 88	if totalAccounts == 0 {
 89		output += "*No accounts yet.*\n"
 90		return output
 91	}
 92
 93	users := ListTopUsersByBalance(1, pageSize)
 94
 95	if len(users) == 0 {
 96		output += "*No data available.*\n"
 97		return output
 98	}
 99
100	output += "| Rank | Address | Balance |\n"
101	output += "|------|---------|--------|\n"
102
103	startRank := 1
104	for i, u := range users {
105		addr := u["user"]
106		balance, _ := strconv.ParseInt(u["balance"], 10, 64)
107		addrLink := addr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + addr + ")]"
108		output += "| " + strconv.Itoa(startRank+i) + " | " + addrLink + " | " + formatAmount(balance) + " |\n"
109	}
110
111	return output
112}
113
114// renderTopByMinted renders paginated list of top holders by minted amount
115func renderTopByMinted(iUrl string) string {
116	output := "# Top Holders by Minted Amount\n\n"
117	output += "[← Home](../acr:)\n\n"
118
119	totalAccounts := acrStore.KnownAccounts()
120	if totalAccounts == 0 {
121		output += "*No accounts yet.*\n"
122		return output
123	}
124
125	users := ListTopUsersByMinting(1, pageSize)
126
127	if len(users) == 0 {
128		output += "*No data available.*\n"
129		return output
130	}
131
132	output += "| Rank | Address | Total Minted |\n"
133	output += "|------|---------|-------------|\n"
134
135	startRank := 1
136	for i, u := range users {
137		addr := u["user"]
138		minted, _ := strconv.ParseInt(u["minted"], 10, 64)
139		addrLink := addr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + addr + ")]"
140		output += "| " + strconv.Itoa(startRank+i) + " | " + addrLink + " | " + formatAmount(minted) + " |\n"
141	}
142
143	return output
144}
145
146// renderAccount renders a single account's ACR details
147func renderAccount(addrStr string) string {
148	addr := address(addrStr)
149	if !addr.IsValid() {
150		return renderError("Invalid address")
151	}
152
153	balance := BalanceOf(addr)
154	minted := MintedOf(addr)
155
156	output := "# Account Details\n\n"
157	output += "[← Home](./acr:)\n\n"
158
159	output += "* **Address**: " + addrStr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + addrStr + ")]\n"
160	output += "* **Balance**: " + formatAmount(balance) + " " + acrStore.GetSymbol() + "\n"
161	output += "* **Total Minted**: " + formatAmount(minted) + " " + acrStore.GetSymbol() + "\n"
162
163	return output
164}
165
166// renderHof renders the Hall of Fame overview page with all categories
167func renderHof() string {
168	output := "# Hall of Fame\n\n"
169	output += "[← Home](./acr:)\n\n"
170
171	categories := ListHofCategories(1, pageSize)
172	if len(categories) == 0 {
173		output += "*No categories yet.*\n"
174		return output
175	}
176
177	for _, name := range categories {
178		csv := GetHofEntries(name)
179		rowCount := 0
180		if csv != "" {
181			rows := strings.Split(csv, "\n")
182			if len(rows) > 1 {
183				rowCount = len(rows) - 1 // exclude header
184			}
185		}
186		output += "* [" + name + "](./acr:hof/" + url.PathEscape(name) + ") (" + strconv.Itoa(rowCount) + " entries)\n"
187	}
188
189	return output
190}
191
192// renderHofCategory renders entries for a specific Hall of Fame category
193func renderHofCategory(name string) string {
194	output := "# Hall of Fame: " + name + "\n\n"
195	output += "[← Hall of Fame](../acr:hof)\n\n"
196
197	csv := GetHofEntries(name)
198	if csv == "" {
199		output += "*No entries yet.*\n"
200		return output
201	}
202
203	rows := strings.Split(csv, "\n")
204	if len(rows) == 0 {
205		output += "*No entries yet.*\n"
206		return output
207	}
208
209	// First row = header
210	headers := strings.Split(rows[0], ",")
211	output += "|"
212	for _, h := range headers {
213		output += " " + h + " |"
214	}
215	output += "\n|"
216	for range headers {
217		output += "------|"
218	}
219	output += "\n"
220
221	// Data rows
222	for i := 1; i < len(rows); i++ {
223		cols := strings.SplitN(rows[i], ",", len(headers))
224		output += "|"
225		for _, c := range cols {
226			output += " " + c + " |"
227		}
228		output += "\n"
229	}
230
231	return output
232}
233
234// renderError renders an error page
235func renderError(message string) string {
236	output := "# Error\n\n"
237	output += "> " + message + "\n\n"
238	output += "[← Back to Home](./acr:)\n"
239	return output
240}
241
242// formatAmount formats token amount with proper decimal places
243func formatAmount(amount int64) string {
244	decimals := acrStore.GetDecimals()
245	if decimals == 0 {
246		return strconv.FormatInt(amount, 10)
247	}
248
249	// Calculate divisor (10^decimals)
250	divisor := int64(1)
251	for i := 0; i < decimals; i++ {
252		divisor *= 10
253	}
254
255	intPart := amount / divisor
256	fracPart := amount % divisor
257
258	return strconv.FormatInt(intPart, 10) + "." + leftPadZeros(strconv.FormatInt(fracPart, 10), int(decimals))
259}
260
261func leftPadZeros(value string, width int) string {
262	for len(value) < width {
263		value = "0" + value
264	}
265	return value
266}