render.gno
14.51 Kb ยท 493 lines
1package gnopendao5
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/nt/commondao"
8 "gno.land/p/nt/ufmt"
9 "gno.land/p/leon/svgbtn"
10 "gno.land/p/moul/txlink"
11)
12
13// ============= RENDER =============
14
15func Render(path string) string {
16 if path == "" {
17 return renderHome()
18 }
19
20 if strings.HasPrefix(path, "listing/") {
21 idStr := strings.TrimPrefix(path, "listing/")
22 id, _ := strconv.Atoi(idStr)
23 return renderListing(id)
24 }
25
26 if strings.HasPrefix(path, "sale/") {
27 idStr := strings.TrimPrefix(path, "sale/")
28 id, _ := strconv.Atoi(idStr)
29 return renderSale(id)
30 }
31
32 if path == "stats" {
33 return renderStats()
34 }
35
36 if path == "proposals" {
37 return renderProposals()
38 }
39
40 if path == "archive" {
41 return renderArchive()
42 }
43
44 if strings.HasPrefix(path, "proposal/") {
45 idStr := strings.TrimPrefix(path, "proposal/")
46 id, _ := strconv.Atoi(idStr)
47 return renderProposal(uint64(id))
48 }
49
50 if strings.HasPrefix(path, "archived/") {
51 idStr := strings.TrimPrefix(path, "archived/")
52 id, _ := strconv.Atoi(idStr)
53 return renderArchivedProposal(uint64(id))
54 }
55
56 return "Page not found"
57}
58
59func renderHome() string {
60 output := "# ๐ช GNOPENSEA DAO 5\n\n"
61 output += "Decentralized NFT Marketplace with DAO Governance\n\n"
62 output += "Compatible **GRC-721** + **GRC-2981** (Automatic Royalties)\n\n"
63 output += "---\n\n"
64 output += ufmt.Sprintf("**Marketplace Address:** %s\n\n", marketplaceAddr.String())
65 output += "---\n\n"
66
67 output += "## Statistics\n\n"
68 output += ufmt.Sprintf("- **Active listings:** %d\n", GetActiveListingsCount())
69 output += ufmt.Sprintf("- **Total sales:** %d\n", GetTotalSales())
70 output += ufmt.Sprintf("- **Total volume:** %s\n", formatPrice(GetTotalVolume()))
71 output += ufmt.Sprintf("- **Royalties paid:** %s\n", formatPrice(GetTotalRoyaltiesPaid()))
72 output += ufmt.Sprintf("- **Marketplace fee:** %s\n", formatFee(marketplaceFee))
73 output += ufmt.Sprintf("- **Balance:** %s\n\n", formatPrice(GetBalance()))
74
75 output += "[View detailed statistics](/r/pierre115/gnopendao5:stats)\n\n"
76 output += "---\n\n"
77
78 // DAO Section
79 output += "## ๐๏ธ DAO Governance\n\n"
80 output += ufmt.Sprintf("**Total DAO Members:** %d\n\n", GetTotalMembers())
81
82 activeProposals := marketplaceDAO.ActiveProposals()
83 if activeProposals.Size() > 0 {
84 output += ufmt.Sprintf("**Active Proposals:** %d\n\n", activeProposals.Size())
85 output += "[๐ View all proposals](/r/pierre115/gnopendao5:proposals)\n\n"
86 } else {
87 output += "**Active Proposals:** 0\n\n"
88 output += "*No active proposals*\n\n"
89 }
90
91 finishedProposals := marketplaceDAO.FinishedProposals()
92 output += ufmt.Sprintf("[๐ View archive (%d)](/r/pierre115/gnopendao:archive)\n\n", finishedProposals.Size())
93
94 // Vote NO button
95 linkjoin := txlink.NewLink("JoinDAO").
96 URL()
97 output += svgbtn.SuccessButton(100, 30, "NO", linkjoin) + "\n\n"
98
99 output += "**Join the DAO:**\n"
100 output += "```bash\n"
101 output += "gnokey maketx call \\\n"
102 output += " -pkgpath \"gno.land/r/pierre115/gnopendao5\" \\\n"
103 output += " -func \"JoinDAO\" \\\n"
104 output += " -send \"1000000ugnot\" \\\n"
105 output += " -broadcast yourkey\n"
106 output += "```\n\n"
107
108 output += "---\n\n"
109
110 // Active listings
111 if GetActiveListingsCount() > 0 {
112 output += "## NFTs for sale\n\n"
113
114 listings.Iterate("", "", func(key string, value interface{}) bool {
115 listing := value.(*Listing)
116 if listing.Active {
117 output += renderListingPreview(listing)
118 }
119 return false
120 })
121 } else {
122 output += "## No NFTs for sale\n\n"
123 output += "*Be the first to list an NFT!*\n"
124 }
125
126 return output
127}
128
129func renderListingPreview(listing *Listing) string {
130 // Calculate breakdown
131 sellerGets, marketFee, royalty, royaltyAddr := GetRoyaltyBreakdown(listing.ListingId)
132
133 output := ufmt.Sprintf("### ๐ท๏ธ Listing #%d\n\n", listing.ListingId)
134 output += ufmt.Sprintf("**Token ID:** %s\n\n", listing.TokenId.String())
135 output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(listing.Price))
136
137 if royalty > 0 {
138 output += ufmt.Sprintf("Royalty: %s (%s)\n\n",
139 formatPrice(royalty),
140 formatPercentage(royalty, listing.Price))
141 }
142
143 output += ufmt.Sprintf("**Seller receives:** %s\n\n", formatPrice(sellerGets))
144 output += ufmt.Sprintf("**Market Fees:** %s\n\n", formatPrice(marketFee))
145 output += ufmt.Sprintf("**Royalty Address:** %s\n\n", royaltyAddr.String())
146 output += ufmt.Sprintf("**Marketplace Address:** %s\n\n", marketplaceAddr.String())
147 output += ufmt.Sprintf("[View details](/r/pierre115/gnopendao5:listing/%d)\n\n", listing.ListingId)
148 output += "---\n\n"
149 return output
150}
151
152func renderListing(listingId int) string {
153 listing := getListing(listingId)
154 if listing == nil {
155 return "# Listing not found"
156 }
157
158 status := "Active"
159 if !listing.Active {
160 status = "Sold/Cancelled"
161 }
162
163 output := ufmt.Sprintf("# Listing #%d - %s\n\n", listingId, status)
164
165 output += "## Details\n\n"
166 output += ufmt.Sprintf("**Token ID:** %s\n\n", listing.TokenId.String())
167 output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(listing.Price))
168 output += ufmt.Sprintf("**Seller:** `%s`\n\n", listing.Seller.String())
169 output += ufmt.Sprintf("**Listed at block:** %d\n\n", listing.ListedAt)
170
171 if listing.Active {
172 sellerGets, marketFee, royalty, royaltyAddr := GetRoyaltyBreakdown(listingId)
173
174 output += "---\n\n"
175 output += "## Price breakdown\n\n"
176 output += ufmt.Sprintf("- **Total price:** %s (100%%)\n", formatPrice(listing.Price))
177 output += ufmt.Sprintf("- **Marketplace fee (%s):** %s\n",
178 formatFee(marketplaceFee), formatPrice(marketFee))
179
180 if royalty > 0 {
181 output += ufmt.Sprintf("- **Creator royalty (%s):** %s\n",
182 formatPercentage(royalty, listing.Price), formatPrice(royalty))
183 output += ufmt.Sprintf(" - Beneficiary: `%s`\n", royaltyAddr.String())
184 } else {
185 output += "- **Royalty:** None\n"
186 }
187
188 output += ufmt.Sprintf("- **Seller receives:** %s (%s)\n\n",
189 formatPrice(sellerGets), formatPercentage(sellerGets, listing.Price))
190
191 output += "## Purchase\n\n"
192 output += "```bash\n"
193 output += "gnokey maketx call \\\n"
194 output += " -pkgpath \"gno.land/r/pierre115/gnopendao5\" \\\n"
195 output += " -func \"BuyNFT\" \\\n"
196 output += ufmt.Sprintf(" -args \"%d\" \\\n", listingId)
197 output += ufmt.Sprintf(" -send \"%dugnot\" \\\n", listing.Price)
198 output += " -broadcast yourkey\n"
199 output += "```\n"
200 }
201
202 output += "\n[โ Back](/r/pierre115/gnopendao5)\n"
203
204 return output
205}
206
207func renderSale(saleId int) string {
208 value, exists := sales.Get(strconv.Itoa(saleId))
209 if !exists {
210 return "# Sale not found"
211 }
212
213 sale := value.(*Sale)
214
215 output := ufmt.Sprintf("# Sale #%d\n\n", saleId)
216 output += ufmt.Sprintf("**Token ID:** %s\n\n", sale.TokenId)
217 output += ufmt.Sprintf("**Price:** %s\n\n", formatPrice(sale.Price))
218 output += ufmt.Sprintf("**Buyer:** `%s`\n\n", sale.Buyer.String())
219 output += ufmt.Sprintf("**Seller:** `%s`\n\n", sale.Seller.String())
220 output += ufmt.Sprintf("**Block:** %d\n\n", sale.SoldAt)
221
222 output += "## ๐ธ Distribution\n\n"
223 output += ufmt.Sprintf("- **Marketplace fee:** %s\n", formatPrice(sale.MarketplaceFee))
224
225 if sale.RoyaltyFee > 0 {
226 output += ufmt.Sprintf("- **Royalty:** %s โ `%s`\n",
227 formatPrice(sale.RoyaltyFee), sale.RoyaltyReceiver.String())
228 }
229
230 sellerReceived := sale.Price - sale.MarketplaceFee - sale.RoyaltyFee
231 output += ufmt.Sprintf("- **Seller received:** %s\n", formatPrice(sellerReceived))
232
233 return output
234}
235
236func renderStats() string {
237 output := "# ๐ Marketplace Statistics\n\n"
238
239 totalVolume := GetTotalVolume()
240 totalSales := GetTotalSales()
241 totalRoyalties := GetTotalRoyaltiesPaid()
242
243 output += ufmt.Sprintf("**Total volume:** %s\n\n", formatPrice(totalVolume))
244 output += ufmt.Sprintf("**Number of sales:** %d\n\n", totalSales)
245 output += ufmt.Sprintf("**Royalties paid:** %s (%s of volume)\n\n",
246 formatPrice(totalRoyalties), formatPercentage(totalRoyalties, totalVolume))
247
248 if totalSales > 0 {
249 avgPrice := totalVolume / int64(totalSales)
250 output += ufmt.Sprintf("**Average price:** %s\n\n", formatPrice(avgPrice))
251 }
252
253 output += "\n[โ Back](/r/pierre115/gnopendao5)\n"
254
255 return output
256}
257
258// ============= DAO RENDER FUNCTIONS =============
259
260func renderProposals() string {
261 output := "# ๐ Active DAO Proposals\n\n"
262
263 proposals := marketplaceDAO.ActiveProposals()
264
265 if proposals.Size() == 0 {
266 output += "*No active proposals at the moment*\n\n"
267 output += "DAO members can create proposals to:\n"
268 output += "- Approve new NFT collections\n"
269 output += "- Remove existing collections\n"
270 output += "- Update marketplace fees\n\n"
271 output += "[โ Back](/r/pierre115/gnopendao5)\n"
272 return output
273 }
274
275 proposals.Iterate(0, proposals.Size(), false, func(p *commondao.Proposal) bool {
276 output += renderProposalPreview(p)
277 return false
278 })
279
280 output += "\n[โ Back](/r/pierre115/gnopendao5)\n"
281
282 return output
283}
284
285func renderProposalPreview(p *commondao.Proposal) string {
286 output := ufmt.Sprintf("## Proposal #%d\n\n", p.ID())
287 output += ufmt.Sprintf("**%s**\n\n", p.Definition().Title())
288
289 // Count votes
290 yesVotes := 0
291 noVotes := 0
292 p.VotingRecord().Iterate(0, p.VotingRecord().Size(), false, func(v commondao.Vote) bool {
293 if string(v.Choice) == "yes" {
294 yesVotes++
295 } else if string(v.Choice) == "no" {
296 noVotes++
297 }
298 return false
299 })
300
301 output += ufmt.Sprintf("**Yes:** %d | **No:** %d | **Total:** %d\n\n", yesVotes, noVotes, yesVotes+noVotes)
302
303 if p.HasVotingDeadlinePassed() {
304 output += "โฐ **Voting ended**\n\n"
305 } else {
306 output += "โ
**Voting open**\n\n"
307 }
308
309 output += ufmt.Sprintf("[View & Vote](/r/pierre115/gnopendao5:proposal/%d)\n\n", p.ID())
310 output += "---\n\n"
311
312 return output
313}
314
315func renderProposal(proposalID uint64) string {
316 proposal := marketplaceDAO.ActiveProposals().Get(proposalID)
317 if proposal == nil {
318 return "# Proposal not found"
319 }
320
321 output := ufmt.Sprintf("# Proposal #%d\n\n", proposalID)
322 output += ufmt.Sprintf("## %s\n\n", proposal.Definition().Title())
323 output += ufmt.Sprintf("%s\n\n", proposal.Definition().Body())
324
325 output += "---\n\n"
326
327 // Vote counts
328 yesVotes := 0
329 noVotes := 0
330 totalVotes := 0
331
332 proposal.VotingRecord().Iterate(0, proposal.VotingRecord().Size(), false, func(v commondao.Vote) bool {
333 if string(v.Choice) == "yes" {
334 yesVotes++
335 } else if string(v.Choice) == "no" {
336 noVotes++
337 }
338 totalVotes++
339 return false
340 })
341
342 output += "## Current Results\n\n"
343 output += ufmt.Sprintf("- **Yes votes:** %d\n", yesVotes)
344 output += ufmt.Sprintf("- **No votes:** %d\n", noVotes)
345 output += ufmt.Sprintf("- **Total votes:** %d\n", totalVotes)
346
347 totalMembers := GetTotalMembers()
348 quorumRequired := (totalMembers * QUORUM) / 100
349 output += ufmt.Sprintf("- **Quorum required:** %d/%d votes\n\n", totalVotes, quorumRequired)
350
351 if proposal.HasVotingDeadlinePassed() {
352 output += "โฐ **Voting period has ended**\n\n"
353 } else {
354 output += "โ
**Voting is open**\n\n"
355
356 output += "---\n\n"
357 output += "## Cast Your Vote\n\n"
358
359 // Vote YES button
360 linkyes := txlink.NewLink("Vote").
361 AddArgs("proposalID", ufmt.Sprintf("%d", proposalID)).
362 AddArgs("choice", "yes").
363 URL()
364 output += svgbtn.SuccessButton(100, 30, "YES", linkyes) + "\n\n"
365
366
367 // Vote NO button
368 linkno := txlink.NewLink("Vote").
369 AddArgs("proposalID", ufmt.Sprintf("%d", proposalID)).
370 AddArgs("choice", "no").
371 URL()
372 output += svgbtn.DangerButton(100, 30, "NO", linkno) + "\n\n"
373 }
374
375 output += "---\n\n"
376 output += "[โ Back to proposals](/r/pierre115/gnopendao:proposals) | [โ Home](/r/pierre115/gnopendao)\n"
377
378 return output
379}
380
381
382// ============= ARCHIVE RENDER FUNCTIONS =============
383
384func renderArchive() string {
385 output := "# ๐ Proposal Archive\n\n"
386
387 finishedProposals := marketplaceDAO.FinishedProposals()
388
389 if finishedProposals.Size() == 0 {
390 output += "*No finished proposals yet*\n\n"
391 output += "[โ Back](/r/pierre115/gnopendao)\n"
392 return output
393 }
394
395 output += ufmt.Sprintf("**Total archived proposals:** %d\n\n", finishedProposals.Size())
396 output += "---\n\n"
397
398 finishedProposals.Iterate(0, finishedProposals.Size(), false, func(p *commondao.Proposal) bool {
399 output += renderArchivedProposalPreview(p)
400 return false
401 })
402
403 output += "\n[โ Back](/r/pierre115/gnopendao)\n"
404
405 return output
406}
407
408func renderArchivedProposalPreview(p *commondao.Proposal) string {
409 output := ufmt.Sprintf("## Proposal #%d - %s\n\n", p.ID(), p.Status())
410 output += ufmt.Sprintf("**%s**\n\n", p.Definition().Title())
411
412 // Count final votes
413 yesVotes := 0
414 noVotes := 0
415 p.VotingRecord().Iterate(0, p.VotingRecord().Size(), false, func(v commondao.Vote) bool {
416 if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
417 yesVotes++
418 } else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
419 noVotes++
420 }
421 return false
422 })
423
424 output += ufmt.Sprintf("**Final Result:** Yes: %d | No: %d\n\n", yesVotes, noVotes)
425
426 statusEmoji := "โ
"
427 if p.Status() == "failed" {
428 statusEmoji = "โ"
429 }
430 output += ufmt.Sprintf("%s **Status:** %s\n\n", statusEmoji, p.Status())
431
432 if p.StatusReason() != "" {
433 output += ufmt.Sprintf("**Reason:** %s\n\n", p.StatusReason())
434 }
435
436 output += ufmt.Sprintf("[View details](/r/pierre115/gnopendao:archived/%d)\n\n", p.ID())
437 output += "---\n\n"
438
439 return output
440}
441
442func renderArchivedProposal(proposalID uint64) string {
443 proposal := marketplaceDAO.FinishedProposals().Get(proposalID)
444 if proposal == nil {
445 return "# Archived proposal not found"
446 }
447
448 output := ufmt.Sprintf("# Proposal #%d - %s\n\n", proposalID, proposal.Status())
449 output += ufmt.Sprintf("## %s\n\n", proposal.Definition().Title())
450 output += ufmt.Sprintf("%s\n\n", proposal.Definition().Body())
451
452 output += "---\n\n"
453
454 // Final vote counts
455 yesVotes := 0
456 noVotes := 0
457 totalVotes := 0
458
459 proposal.VotingRecord().Iterate(0, proposal.VotingRecord().Size(), false, func(v commondao.Vote) bool {
460 if string(v.Choice) == "yes" || string(v.Choice) == "YES" {
461 yesVotes++
462 } else if string(v.Choice) == "no" || string(v.Choice) == "NO" {
463 noVotes++
464 }
465 totalVotes++
466 return false
467 })
468
469 output += "## Final Results\n\n"
470 output += ufmt.Sprintf("- **Yes votes:** %d\n", yesVotes)
471 output += ufmt.Sprintf("- **No votes:** %d\n", noVotes)
472 output += ufmt.Sprintf("- **Total votes:** %d\n", totalVotes)
473
474 totalMembers := GetTotalMembers()
475 quorumRequired := (totalMembers * QUORUM) / 100
476 output += ufmt.Sprintf("- **Quorum required:** %d/%d votes\n\n", totalVotes, quorumRequired)
477
478 statusEmoji := "โ
"
479 if proposal.Status() == "failed" {
480 statusEmoji = "โ"
481 }
482 output += ufmt.Sprintf("%s **Final Status:** %s\n\n", statusEmoji, proposal.Status())
483
484 if proposal.StatusReason() != "" {
485 output += ufmt.Sprintf("**Status Reason:** %s\n\n", proposal.StatusReason())
486 }
487
488 output += ufmt.Sprintf("**Voting Ended:** %s\n\n", proposal.VotingDeadline().Format("2006-01-02 15:04:05"))
489
490 output += "---\n\n"
491 output += "[โ Back to archive](/r/pierre115/gnopendao:archive) | [โ Home](/r/pierre115/gnopendao)\n"
492
493 return output
494}