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 +}