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}