From b49ca9955a8e9b99f9afc89f265cad5a8ffb55da Mon Sep 17 00:00:00 2001
From: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Date: Mon, 2 Dec 2024 09:31:45 +0100
Subject: [PATCH] feat(govdao): better rendering (#3096)
## Description
Introduces better `gnoweb` rendering for the GovDAO suite, and better
help page actions for voting on proposals.
Home page before:
Home page after (also resolves usernames from `r/demo/users`):
Prop page before:
Prop page after:
The actions bar notifies the user when the proposal is no longer active
as well.
Continuation of #2579
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---
examples/gno.land/p/demo/dao/dao.gno | 1 +
examples/gno.land/p/demo/dao/proposals.gno | 5 +-
examples/gno.land/p/demo/simpledao/dao.gno | 8 +
.../gno.land/p/demo/simpledao/dao_test.gno | 49 ++++++
.../gno.land/p/demo/simpledao/propstore.gno | 38 +++--
examples/gno.land/r/gov/dao/v2/dao.gno | 54 ------
examples/gno.land/r/gov/dao/v2/gno.mod | 2 +
.../gno.land/r/gov/dao/v2/prop1_filetest.gno | 80 +++++++--
.../gno.land/r/gov/dao/v2/prop2_filetest.gno | 80 +++++++--
.../gno.land/r/gov/dao/v2/prop3_filetest.gno | 98 +++++++++--
.../gno.land/r/gov/dao/v2/prop4_filetest.gno | 155 ++++++++----------
examples/gno.land/r/gov/dao/v2/render.gno | 123 ++++++++++++++
12 files changed, 485 insertions(+), 208 deletions(-)
create mode 100644 examples/gno.land/r/gov/dao/v2/render.gno
diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno
index f8ea433192f..e3a2ba72c5b 100644
--- a/examples/gno.land/p/demo/dao/dao.gno
+++ b/examples/gno.land/p/demo/dao/dao.gno
@@ -15,6 +15,7 @@ const (
// that contains the necessary information to
// log and generate a valid proposal
type ProposalRequest struct {
+ Title string // the title associated with the proposal
Description string // the description associated with the proposal
Executor Executor // the proposal executor
}
diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno
index 5cad679d006..66abcb248c5 100644
--- a/examples/gno.land/p/demo/dao/proposals.gno
+++ b/examples/gno.land/p/demo/dao/proposals.gno
@@ -16,7 +16,7 @@ var (
Accepted ProposalStatus = "accepted" // proposal gathered quorum
NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum
ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully
- ExecutionFailed ProposalStatus = "execution failed" // proposal is failed during execution
+ ExecutionFailed ProposalStatus = "execution failed" // proposal has failed during execution
)
func (s ProposalStatus) String() string {
@@ -42,6 +42,9 @@ type Proposal interface {
// Author returns the author of the proposal
Author() std.Address
+ // Title returns the title of the proposal
+ Title() string
+
// Description returns the description of the proposal
Description() string
diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno
index 7a20237ec3f..837f64a41d6 100644
--- a/examples/gno.land/p/demo/simpledao/dao.gno
+++ b/examples/gno.land/p/demo/simpledao/dao.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/avl"
"gno.land/p/demo/dao"
@@ -12,6 +13,7 @@ import (
var (
ErrInvalidExecutor = errors.New("invalid executor provided")
+ ErrInvalidTitle = errors.New("invalid proposal title provided")
ErrInsufficientProposalFunds = errors.New("insufficient funds for proposal")
ErrInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal")
ErrProposalExecuted = errors.New("proposal already executed")
@@ -47,6 +49,11 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
return 0, ErrInvalidExecutor
}
+ // Make sure the title is set
+ if strings.TrimSpace(request.Title) == "" {
+ return 0, ErrInvalidTitle
+ }
+
var (
caller = getDAOCaller()
sentCoins = std.GetOrigSend() // Get the sent coins, if any
@@ -61,6 +68,7 @@ func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {
// Create the wrapped proposal
prop := &proposal{
author: caller,
+ title: request.Title,
description: request.Description,
executor: request.Executor,
status: dao.Active,
diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno
index fb32895e72f..46251e24dad 100644
--- a/examples/gno.land/p/demo/simpledao/dao_test.gno
+++ b/examples/gno.land/p/demo/simpledao/dao_test.gno
@@ -45,6 +45,50 @@ func TestSimpleDAO_Propose(t *testing.T) {
)
})
+ t.Run("invalid title", func(t *testing.T) {
+ t.Parallel()
+
+ var (
+ called = false
+ cb = func() error {
+ called = true
+
+ return nil
+ }
+ ex = &mockExecutor{
+ executeFn: cb,
+ }
+
+ sentCoins = std.NewCoins(
+ std.NewCoin(
+ "ugnot",
+ minProposalFeeValue,
+ ),
+ )
+
+ ms = &mockMemberStore{
+ isMemberFn: func(_ std.Address) bool {
+ return false
+ },
+ }
+ s = New(ms)
+ )
+
+ std.TestSetOrigSend(sentCoins, std.Coins{})
+
+ _, err := s.Propose(dao.ProposalRequest{
+ Executor: ex,
+ Title: "", // Set invalid title
+ })
+ uassert.ErrorIs(
+ t,
+ err,
+ ErrInvalidTitle,
+ )
+
+ uassert.False(t, called)
+ })
+
t.Run("caller cannot cover fee", func(t *testing.T) {
t.Parallel()
@@ -58,6 +102,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
ex = &mockExecutor{
executeFn: cb,
}
+ title = "Proposal title"
sentCoins = std.NewCoins(
std.NewCoin(
@@ -80,6 +125,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
_, err := s.Propose(dao.ProposalRequest{
Executor: ex,
+ Title: title,
})
uassert.ErrorIs(
t,
@@ -105,6 +151,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
executeFn: cb,
}
description = "Proposal description"
+ title = "Proposal title"
proposer = testutils.TestAddress("proposer")
sentCoins = std.NewCoins(
@@ -129,6 +176,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
// Make sure the proposal was added
id, err := s.Propose(dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
})
@@ -141,6 +189,7 @@ func TestSimpleDAO_Propose(t *testing.T) {
uassert.Equal(t, proposer.String(), prop.Author().String())
uassert.Equal(t, description, prop.Description())
+ uassert.Equal(t, title, prop.Title())
uassert.Equal(t, dao.Active.String(), prop.Status().String())
stats := prop.Stats()
diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno
index 06741d397cb..91f2a883047 100644
--- a/examples/gno.land/p/demo/simpledao/propstore.gno
+++ b/examples/gno.land/p/demo/simpledao/propstore.gno
@@ -3,6 +3,7 @@ package simpledao
import (
"errors"
"std"
+ "strings"
"gno.land/p/demo/dao"
"gno.land/p/demo/seqid"
@@ -18,6 +19,7 @@ const maxRequestProposals = 10
// proposal is the internal simpledao proposal implementation
type proposal struct {
author std.Address // initiator of the proposal
+ title string // title of the proposal
description string // description of the proposal
executor dao.Executor // executor for the proposal
@@ -31,6 +33,10 @@ func (p *proposal) Author() std.Address {
return p.author
}
+func (p *proposal) Title() string {
+ return p.title
+}
+
func (p *proposal) Description() string {
return p.description
}
@@ -63,15 +69,20 @@ func (p *proposal) Render() string {
// Fetch the voting stats
stats := p.Stats()
- output := ""
- output += ufmt.Sprintf("Author: %s", p.Author().String())
- output += "\n\n"
- output += p.Description()
- output += "\n\n"
- output += ufmt.Sprintf("Status: %s", p.Status().String())
- output += "\n\n"
- output += ufmt.Sprintf(
- "Voting stats: YES %d (%d%%), NO %d (%d%%), ABSTAIN %d (%d%%), MISSING VOTE %d (%d%%)",
+ var out string
+
+ out += "## Description\n\n"
+ if strings.TrimSpace(p.description) != "" {
+ out += ufmt.Sprintf("%s\n\n", p.description)
+ } else {
+ out += "No description provided.\n\n"
+ }
+
+ out += "## Proposal information\n\n"
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(p.Status().String()))
+
+ out += ufmt.Sprintf(
+ "**Voting stats:**\n- YES %d (%d%%)\n- NO %d (%d%%)\n- ABSTAIN %d (%d%%)\n- MISSING VOTES %d (%d%%)\n",
stats.YayVotes,
stats.YayPercent(),
stats.NayVotes,
@@ -81,10 +92,13 @@ func (p *proposal) Render() string {
stats.MissingVotes(),
stats.MissingVotesPercent(),
)
- output += "\n\n"
- output += ufmt.Sprintf("Threshold met: %t", stats.YayVotes > (2*stats.TotalVotingPower)/3)
- return output
+ out += "\n\n"
+ thresholdOut := strings.ToUpper(ufmt.Sprintf("%t", stats.YayVotes > (2*stats.TotalVotingPower)/3))
+
+ out += ufmt.Sprintf("**Threshold met: %s**\n\n", thresholdOut)
+
+ return out
}
// addProposal adds a new simpledao proposal to the store
diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno
index d99a161bcdf..9263d8d440b 100644
--- a/examples/gno.land/r/gov/dao/v2/dao.gno
+++ b/examples/gno.land/r/gov/dao/v2/dao.gno
@@ -2,12 +2,10 @@ package govdao
import (
"std"
- "strconv"
"gno.land/p/demo/dao"
"gno.land/p/demo/membstore"
"gno.land/p/demo/simpledao"
- "gno.land/p/demo/ufmt"
)
var (
@@ -65,55 +63,3 @@ func GetPropStore() dao.PropStore {
func GetMembStore() membstore.MemberStore {
return members
}
-
-func Render(path string) string {
- if path == "" {
- numProposals := d.Size()
-
- if numProposals == 0 {
- return "No proposals found :(" // corner case
- }
-
- output := ""
-
- offset := uint64(0)
- if numProposals >= 10 {
- offset = uint64(numProposals) - 10
- }
-
- // Fetch the last 10 proposals
- for idx, prop := range d.Proposals(offset, uint64(10)) {
- output += ufmt.Sprintf(
- "- [Proposal #%d](%s:%d) - (**%s**)(by %s)\n",
- idx,
- "/r/gov/dao/v2",
- idx,
- prop.Status().String(),
- prop.Author().String(),
- )
- }
-
- return output
- }
-
- // Display the detailed proposal
- idx, err := strconv.Atoi(path)
- if err != nil {
- return "404: Invalid proposal ID"
- }
-
- // Fetch the proposal
- prop, err := d.ProposalByID(uint64(idx))
- if err != nil {
- return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
- }
-
- // Render the proposal
- output := ""
- output += ufmt.Sprintf("# Prop #%d", idx)
- output += "\n\n"
- output += prop.Render()
- output += "\n\n"
-
- return output
-}
diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod
index bc379bf18df..4da6e0a2484 100644
--- a/examples/gno.land/r/gov/dao/v2/gno.mod
+++ b/examples/gno.land/r/gov/dao/v2/gno.mod
@@ -7,4 +7,6 @@ require (
gno.land/p/demo/simpledao v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/gov/executor v0.0.0-latest
+ gno.land/p/moul/txlink v0.0.0-latest
+ gno.land/r/demo/users v0.0.0-latest
)
diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
index 7b25eeb1db3..7d8975e1fe8 100644
--- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno
@@ -42,9 +42,11 @@ func init() {
executor := validators.NewPropExecutor(changesFn)
// Create a proposal
+ title := "Valset change"
description := "manual valset changes proposal example"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: executor,
}
@@ -73,52 +75,98 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Valset change](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// No valset changes to apply.
// --
// --
-// # Prop #0
+// # Proposal #0 - Valset change
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// manual valset changes proposal example
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
index 4eb993b80dc..84a64bc4ee2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno
@@ -19,9 +19,11 @@ func init() {
)
// Create a proposal
+ title := "govdao blog post title"
description := "post a new blogpost about govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: ex,
}
@@ -50,35 +52,68 @@ func main() {
// Output:
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
@@ -87,17 +122,30 @@ func main() {
// No posts.
// --
// --
-// # Prop #0
+// # Proposal #0 - govdao blog post title
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// post a new blogpost about govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
index 546213431e4..068f520e7e2 100644
--- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno
@@ -28,9 +28,11 @@ func init() {
}
// Create a proposal
+ title := "new govdao member addition"
description := "add new members to the govdao"
prop := dao.ProposalRequest{
+ Title: title,
Description: description,
Executor: govdao.NewMemberPropExecutor(memberFn),
}
@@ -65,57 +67,117 @@ func main() {
// --
// 1
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
+//
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
+//
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
+//
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// ### Actions
//
-// Threshold met: true
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**accepted**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: ACCEPTED**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - new govdao member addition
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// add new members to the govdao
//
-// Status: execution successful
+// ## Proposal information
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Voting stats:**
+// - YES 10 (25%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 30 (75%)
+//
//
-// Voting stats: YES 10 (25%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 30 (75%)
+// **Threshold met: FALSE**
//
-// Threshold met: false
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**execution successful**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)
+//
+// **Status: EXECUTION SUCCESSFUL**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
// 4
diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
index 8eff79ffb5a..13ca572c512 100644
--- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
+++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno
@@ -9,8 +9,10 @@ import (
func init() {
mExec := params.NewStringPropExecutor("prop1.string", "value1")
+ title := "Setting prop1.string param"
comment := "setting prop1.string param"
prop := dao.ProposalRequest{
+ Title: title,
Description: comment,
Executor: mExec,
}
@@ -36,124 +38,95 @@ func main() {
// Output:
// new prop 0
// --
-// - [Proposal #0](/r/gov/dao/v2:0) - (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm)
+// # GovDAO Proposals
+//
+// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0)
+//
+// **Status: ACTIVE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
//
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: active
+// ## Proposal information
+//
+// **Status: ACTIVE**
//
-// Voting stats: YES 0 (0%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 10 (100%)
+// **Voting stats:**
+// - YES 0 (0%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 10 (100%)
//
-// Threshold met: false
+//
+// **Threshold met: FALSE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)]
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: accepted
+// ## Proposal information
+//
+// **Status: ACCEPTED**
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
//
-// Threshold met: true
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
// --
// --
-// # Prop #0
+// # Proposal #0 - Setting prop1.string param
//
-// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm
+// ## Description
//
// setting prop1.string param
//
-// Status: execution successful
+// ## Proposal information
//
-// Voting stats: YES 10 (100%), NO 0 (0%), ABSTAIN 0 (0%), MISSING VOTE 0 (0%)
+// **Status: EXECUTION SUCCESSFUL**
//
-// Threshold met: true
+// **Voting stats:**
+// - YES 10 (100%)
+// - NO 0 (0%)
+// - ABSTAIN 0 (0%)
+// - MISSING VOTES 0 (0%)
+//
+//
+// **Threshold met: TRUE**
+//
+// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**
+//
+// ### Actions
+//
+// The voting period for this proposal is over.
//
//
-
-// Events:
-// [
-// {
-// "type": "ProposalAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "proposal-author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAdded"
-// },
-// {
-// "type": "VoteAdded",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "author",
-// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"
-// },
-// {
-// "key": "option",
-// "value": "YES"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitVoteAdded"
-// },
-// {
-// "type": "ProposalAccepted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "EmitProposalAccepted"
-// },
-// {
-// "type": "set",
-// "attrs": [
-// {
-// "key": "k",
-// "value": "prop1.string"
-// }
-// ],
-// "pkg_path": "gno.land/r/sys/params",
-// "func": ""
-// },
-// {
-// "type": "ProposalExecuted",
-// "attrs": [
-// {
-// "key": "proposal-id",
-// "value": "0"
-// },
-// {
-// "key": "exec-status",
-// "value": "accepted"
-// }
-// ],
-// "pkg_path": "gno.land/r/gov/dao/v2",
-// "func": "ExecuteProposal"
-// }
-// ]
diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno
new file mode 100644
index 00000000000..4cca397e851
--- /dev/null
+++ b/examples/gno.land/r/gov/dao/v2/render.gno
@@ -0,0 +1,123 @@
+package govdao
+
+import (
+ "strconv"
+ "strings"
+
+ "gno.land/p/demo/dao"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+ "gno.land/r/demo/users"
+)
+
+func Render(path string) string {
+ var out string
+
+ if path == "" {
+ out += "# GovDAO Proposals\n\n"
+ numProposals := d.Size()
+
+ if numProposals == 0 {
+ out += "No proposals found :(" // corner case
+ return out
+ }
+
+ offset := uint64(0)
+ if numProposals >= 10 {
+ offset = uint64(numProposals) - 10
+ }
+
+ // Fetch the last 10 proposals
+ proposals := d.Proposals(offset, uint64(10))
+ for i := len(proposals) - 1; i >= 0; i-- {
+ prop := proposals[i]
+
+ title := prop.Title()
+ if len(title) > 40 {
+ title = title[:40] + "..."
+ }
+
+ propID := offset + uint64(i)
+ out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID)
+ out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String()))
+
+ user := users.GetUserByAddress(prop.Author())
+ authorDisplayText := prop.Author().String()
+ if user != nil {
+ authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name)
+ }
+
+ out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText)
+
+ if i != 0 {
+ out += "---\n\n"
+ }
+ }
+
+ return out
+ }
+
+ // Display the detailed proposal
+ idx, err := strconv.Atoi(path)
+ if err != nil {
+ return "404: Invalid proposal ID"
+ }
+
+ // Fetch the proposal
+ prop, err := d.ProposalByID(uint64(idx))
+ if err != nil {
+ return ufmt.Sprintf("unable to fetch proposal, %s", err.Error())
+ }
+
+ // Render the proposal page
+ out += renderPropPage(prop, idx)
+
+ return out
+}
+
+func renderPropPage(prop dao.Proposal, idx int) string {
+ var out string
+
+ out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title())
+ out += prop.Render()
+ out += renderAuthor(prop)
+ out += renderActionBar(prop, idx)
+ out += "\n\n"
+
+ return out
+}
+
+func renderAuthor(p dao.Proposal) string {
+ var out string
+
+ authorUsername := ""
+ user := users.GetUserByAddress(p.Author())
+ if user != nil {
+ authorUsername = user.Name
+ }
+
+ if authorUsername != "" {
+ out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername)
+ } else {
+ out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String())
+ }
+
+ return out
+}
+
+func renderActionBar(p dao.Proposal, idx int) string {
+ var out string
+
+ out += "### Actions\n\n"
+ if p.Status() == dao.Active {
+ out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]",
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"),
+ txlink.URL("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"),
+ )
+ } else {
+ out += "The voting period for this proposal is over."
+ }
+
+ return out
+}