From 84294d3f0c09c0cf750f565448986a39e55f92a9 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:01:08 +0200 Subject: [PATCH 01/18] feat(examples): add p/moul/udao package Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao.gno | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 examples/gno.land/p/moul/udao/udao.gno diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno new file mode 100644 index 00000000000..b4dc597d26a --- /dev/null +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -0,0 +1,77 @@ +// Package udao defines minimal interfaces for Decentralized Autonomous Organizations (DAOs). +// It intentionally does not expose members and votes, as these details are implementation-specific. +// Instead, it focuses on providing an external view of proposals and their statuses, +// which is what non-members and non-voters typically care about. +// +// The package is designed to allow for composable DAO patterns, enabling flexible +// and modular implementations of various DAO structures and behaviors. +package udao + +// DAO defines a minimal interface for a Decentralized Autonomous Organization +// from an external point of view, hiding the internal details of members and voting. +type DAO interface { + // Propose submits a new proposal to the DAO + Propose(proposal Proposal) (uint64, error) + + // GetProposalStatus retrieves the current status and metrics of a specific proposal + GetProposalStatus(proposalID uint64) (ProposalStatus, error) + + // Execute attempts to execute a proposal if it has passed + Execute(proposalID uint64) error + + // GetProposal retrieves the details of a specific proposal + GetProposal(proposalID uint64) (Proposal, error) + + // XXX: find a smart way to list proposals + // // ListProposalss retrieves a list of Proposals with pagination and optional filters + // ListDAOs(offset, limit int, filters ...ProposalFilter) ([]DAO, error) +} + +// Proposal defines the interface for a DAO proposal +type Proposal interface { + Title() string + Body() string + Constraints() []Constraint +} + +// ProposalStatus represents the current status and metrics of a proposal +type ProposalStatus struct { + // status + State ProposalState + + // metrics + YayPercentage float64 + NayPercentage float64 + NonVoterPercentage float64 + // XXX: other metrics? +} + +// ProposalState represents the current state of a proposal +type ProposalState int + +const ( + Pending ProposalState = iota + Active + Passed + Rejected + Executed + Expired // XXX: better name for when it's expired but not because of time? +) + +// Constraint defines an interface for proposal constraints +type Constraint interface { + Validate() (bool, string) + Description() string +} + +// ValidateConstraints checks if all constraints of a proposal are met +func ValidateConstraints(p Proposal) (bool, []string) { + var unmetReasons []string + for _, constraint := range p.Constraints() { + valid, reason := constraint.Validate() + if !valid { + unmetReasons = append(unmetReasons, reason) + } + } + return len(unmetReasons) == 0, unmetReasons +} From d5b3217e51070a2ddb133a590b76307d5f6f3e8e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:50:15 +0200 Subject: [PATCH 02/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/gno.mod | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/gno.land/p/moul/udao/gno.mod diff --git a/examples/gno.land/p/moul/udao/gno.mod b/examples/gno.land/p/moul/udao/gno.mod new file mode 100644 index 00000000000..353db7e20e0 --- /dev/null +++ b/examples/gno.land/p/moul/udao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/udao From a79e8dc321a281503f1445bc703ac1042d3aa7a9 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:11:55 +0100 Subject: [PATCH 03/18] Update examples/gno.land/p/moul/udao/udao.gno Co-authored-by: Mikael VALLENET --- examples/gno.land/p/moul/udao/udao.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index b4dc597d26a..dfd09cec55c 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -40,7 +40,7 @@ type ProposalStatus struct { State ProposalState // metrics - YayPercentage float64 + YeaPercentage float64 NayPercentage float64 NonVoterPercentage float64 // XXX: other metrics? From 58443bfcc1493049697d009cc07f7390504ebdb2 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:47:45 +0100 Subject: [PATCH 04/18] Update examples/gno.land/p/moul/udao/udao.gno --- examples/gno.land/p/moul/udao/udao.gno | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index dfd09cec55c..92a8d59a9bf 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -42,7 +42,8 @@ type ProposalStatus struct { // metrics YeaPercentage float64 NayPercentage float64 - NonVoterPercentage float64 + AbstainPercentage float64 + PendingVoterPercentage float64 // XXX: other metrics? } From 4cf3e543c63a167335686a788c1c03ce6ca216cc Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:48:27 +0100 Subject: [PATCH 05/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao.gno | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index 92a8d59a9bf..3d73cc33471 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -40,9 +40,9 @@ type ProposalStatus struct { State ProposalState // metrics - YeaPercentage float64 - NayPercentage float64 - AbstainPercentage float64 + YeaPercentage float64 + NayPercentage float64 + AbstainPercentage float64 PendingVoterPercentage float64 // XXX: other metrics? } From 3cd956e0a4e1e6d410c65ba3645747b514231632 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:40:48 +0100 Subject: [PATCH 06/18] chore: fixup --- examples/gno.land/p/moul/udao/udao.gno | 55 ++++++++++++-------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index 3d73cc33471..0e37453ac76 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -7,37 +7,34 @@ // and modular implementations of various DAO structures and behaviors. package udao +import "time" + // DAO defines a minimal interface for a Decentralized Autonomous Organization -// from an external point of view, hiding the internal details of members and voting. type DAO interface { // Propose submits a new proposal to the DAO - Propose(proposal Proposal) (uint64, error) + Propose(proposal PropDefinition) (uint64, error) - // GetProposalStatus retrieves the current status and metrics of a specific proposal - GetProposalStatus(proposalID uint64) (ProposalStatus, error) + // GetProposal retrieves both the definition and status of a specific proposal + GetProposal(proposalID uint64) (PropDefinition, PropStatus, error) // Execute attempts to execute a proposal if it has passed Execute(proposalID uint64) error - // GetProposal retrieves the details of a specific proposal - GetProposal(proposalID uint64) (Proposal, error) - // XXX: find a smart way to list proposals - // // ListProposalss retrieves a list of Proposals with pagination and optional filters - // ListDAOs(offset, limit int, filters ...ProposalFilter) ([]DAO, error) } -// Proposal defines the interface for a DAO proposal -type Proposal interface { +// PropDefinition defines the interface for a DAO proposal +type PropDefinition interface { Title() string Body() string + Created() time.Time Constraints() []Constraint } -// ProposalStatus represents the current status and metrics of a proposal -type ProposalStatus struct { +// PropStatus represents the current status and metrics of a proposal +type PropStatus struct { // status - State ProposalState + State PropState // metrics YeaPercentage float64 @@ -47,32 +44,30 @@ type ProposalStatus struct { // XXX: other metrics? } -// ProposalState represents the current state of a proposal -type ProposalState int +// PropState represents the current state of a proposal +type PropState string const ( - Pending ProposalState = iota - Active - Passed - Rejected - Executed - Expired // XXX: better name for when it's expired but not because of time? + Pending PropState = "pending" + Active PropState = "active" + Passed PropState = "passed" + Rejected PropState = "rejected" + Executed PropState = "executed" + Expired PropState = "expired" ) // Constraint defines an interface for proposal constraints type Constraint interface { - Validate() (bool, string) - Description() string + Validate() (valid bool, description string) } // ValidateConstraints checks if all constraints of a proposal are met func ValidateConstraints(p Proposal) (bool, []string) { - var unmetReasons []string - for _, constraint := range p.Constraints() { - valid, reason := constraint.Validate() - if !valid { - unmetReasons = append(unmetReasons, reason) + reasons := make([]string, 0) + for _, c := range p.Constraints() { + if valid, reason := c.Validate(); !valid { + reasons = append(reasons, reason) } } - return len(unmetReasons) == 0, unmetReasons + return len(reasons) == 0, reasons } From 80484f062bf083a27b8ecad53ae263599235b2b4 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:46:08 +0100 Subject: [PATCH 07/18] feat(examples): add rolist Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/list/list.gno | 16 ++ examples/gno.land/p/demo/avl/rolist/gno.mod | 1 + .../gno.land/p/demo/avl/rolist/rolist.gno | 118 +++++++++++++ .../p/demo/avl/rolist/rolist_test.gno | 162 ++++++++++++++++++ .../gno.land/p/demo/avl/rotree/rotree.gno | 19 +- 5 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 examples/gno.land/p/demo/avl/rolist/gno.mod create mode 100644 examples/gno.land/p/demo/avl/rolist/rolist.gno create mode 100644 examples/gno.land/p/demo/avl/rolist/rolist_test.gno diff --git a/examples/gno.land/p/demo/avl/list/list.gno b/examples/gno.land/p/demo/avl/list/list.gno index 0875eb66e01..594f5fa2a1f 100644 --- a/examples/gno.land/p/demo/avl/list/list.gno +++ b/examples/gno.land/p/demo/avl/list/list.gno @@ -41,6 +41,22 @@ import ( "gno.land/p/demo/seqid" ) +// IList defines the interface for list operations +type IList interface { + Len() int + Append(values ...interface{}) + Get(index int) interface{} + Set(index int, value interface{}) bool + Delete(index int) (interface{}, bool) + Slice(startIndex, endIndex int) []interface{} + ForEach(fn func(index int, value interface{}) bool) + Clone() *List + DeleteRange(startIndex, endIndex int) int +} + +// Verify List implements IList interface +var _ IList = (*List)(nil) + // List represents an ordered sequence of items backed by an AVL tree type List struct { tree avl.Tree diff --git a/examples/gno.land/p/demo/avl/rolist/gno.mod b/examples/gno.land/p/demo/avl/rolist/gno.mod new file mode 100644 index 00000000000..6461f22c51c --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/rolist \ No newline at end of file diff --git a/examples/gno.land/p/demo/avl/rolist/rolist.gno b/examples/gno.land/p/demo/avl/rolist/rolist.gno new file mode 100644 index 00000000000..d72f7465288 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/rolist.gno @@ -0,0 +1,118 @@ +// Package rolist provides a read-only wrapper for list.List with safe value transformation. +// +// It is useful when you want to expose a read-only view of a list while ensuring that +// the sensitive data cannot be modified. +// +// Example: +// +// // Define a user structure with sensitive data +// type User struct { +// Name string +// Balance int +// Internal string // sensitive field +// } +// +// // Create and populate the original list +// privateList := list.New() +// privateList.Append(&User{ +// Name: "Alice", +// Balance: 100, +// Internal: "sensitive", +// }) +// +// // Create a safe transformation function that copies the struct +// // while excluding sensitive data +// makeEntrySafeFn := func(v interface{}) interface{} { +// u := v.(*User) +// return &User{ +// Name: u.Name, +// Balance: u.Balance, +// Internal: "", // omit sensitive data +// } +// } +// +// // Create a read-only view of the list +// publicList := rolist.Wrap(list, makeEntrySafeFn) +// +// // Safely access the data +// value := publicList.Get(0) +// user := value.(*User) +// // user.Name == "Alice" +// // user.Balance == 100 +// // user.Internal == "" (sensitive data is filtered) +package rolist + +import ( + "gno.land/p/demo/avl/list" +) + +// IReadOnlyList defines the read-only operations available on a list. +type IReadOnlyList interface { + Len() int + Get(index int) interface{} + Slice(startIndex, endIndex int) []interface{} + ForEach(fn func(index int, value interface{}) bool) +} + +// ReadOnlyList wraps a list.List and provides read-only access. +type ReadOnlyList struct { + list *list.List + makeEntrySafeFn func(interface{}) interface{} +} + +// Verify that ReadOnlyList implements IReadOnlyList +var _ IReadOnlyList = (*ReadOnlyList)(nil) + +// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function. +// If makeEntrySafeFn is nil, values will be returned as-is without transformation. +func Wrap(list *list.List, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyList { + return &ReadOnlyList{ + list: list, + makeEntrySafeFn: makeEntrySafeFn, + } +} + +// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value +func (rol *ReadOnlyList) getSafeValue(value interface{}) interface{} { + if rol.makeEntrySafeFn == nil { + return value + } + return rol.makeEntrySafeFn(value) +} + +// Len returns the number of elements in the list. +func (rol *ReadOnlyList) Len() int { + return rol.list.Len() +} + +// Get returns the value at the specified index, converted to a safe format. +// Returns nil if index is out of bounds. +func (rol *ReadOnlyList) Get(index int) interface{} { + value := rol.list.Get(index) + if value == nil { + return nil + } + return rol.getSafeValue(value) +} + +// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive), +// with all values converted to a safe format. +func (rol *ReadOnlyList) Slice(startIndex, endIndex int) []interface{} { + values := rol.list.Slice(startIndex, endIndex) + if values == nil { + return nil + } + + result := make([]interface{}, len(values)) + for i, v := range values { + result[i] = rol.getSafeValue(v) + } + return result +} + +// ForEach iterates through all elements in the list, providing safe versions of the values. +func (rol *ReadOnlyList) ForEach(fn func(index int, value interface{}) bool) { + rol.list.ForEach(func(index int, value interface{}) bool { + return fn(index, rol.getSafeValue(value)) + }) +} diff --git a/examples/gno.land/p/demo/avl/rolist/rolist_test.gno b/examples/gno.land/p/demo/avl/rolist/rolist_test.gno new file mode 100644 index 00000000000..03b0a8cba30 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/rolist_test.gno @@ -0,0 +1,162 @@ +package rolist + +import ( + "testing" + + "gno.land/p/demo/avl/list" +) + +func TestExample(t *testing.T) { + // User represents our internal data structure + type User struct { + ID string + Name string + Balance int + Internal string // sensitive internal data + } + + // Create and populate the original list + l := &list.List{} + l.Append( + &User{ + ID: "1", + Name: "Alice", + Balance: 100, + Internal: "sensitive_data_1", + }, + &User{ + ID: "2", + Name: "Bob", + Balance: 200, + Internal: "sensitive_data_2", + }, + ) + + // Define a makeEntrySafeFn that: + // 1. Creates a defensive copy of the User struct + // 2. Omits sensitive internal data + makeEntrySafeFn := func(v interface{}) interface{} { + originalUser := v.(*User) + return &User{ + ID: originalUser.ID, + Name: originalUser.Name, + Balance: originalUser.Balance, + Internal: "", // Omit sensitive data + } + } + + // Create a read-only view of the list + roList := Wrap(l, makeEntrySafeFn) + + // Test retrieving and verifying a user + t.Run("Get User", func(t *testing.T) { + // Get user from read-only list + value := roList.Get(0) + if value == nil { + t.Fatal("User at index 0 not found") + } + + user := value.(*User) + + // Verify user data is correct + if user.Name != "Alice" || user.Balance != 100 { + t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) + } + + // Verify sensitive data is not exposed + if user.Internal != "" { + t.Error("Sensitive data should not be exposed") + } + + // Verify it's a different instance than the original + originalUser := l.Get(0).(*User) + if user == originalUser { + t.Error("Read-only list should return a copy, not the original pointer") + } + }) + + // Test slice functionality + t.Run("Slice Users", func(t *testing.T) { + users := roList.Slice(0, 2) + if len(users) != 2 { + t.Fatalf("Expected 2 users, got %d", len(users)) + } + + for _, v := range users { + user := v.(*User) + if user.Internal != "" { + t.Error("Sensitive data exposed in slice") + } + } + }) + + // Test ForEach functionality + t.Run("ForEach Users", func(t *testing.T) { + count := 0 + roList.ForEach(func(index int, value interface{}) bool { + user := value.(*User) + if user.Internal != "" { + t.Error("Sensitive data exposed during iteration") + } + count++ + return false + }) + + if count != 2 { + t.Errorf("Expected 2 users, got %d", count) + } + }) +} + +func TestNilMakeEntrySafeFn(t *testing.T) { + // Create a list with some test data + l := &list.List{} + originalValue := []int{1, 2, 3} + l.Append(originalValue) + + // Create a ReadOnlyList with nil makeEntrySafeFn + roList := Wrap(l, nil) + + // Test that we get back the original value + value := roList.Get(0) + if value == nil { + t.Fatal("Value not found") + } + + // Verify it's the exact same slice (not a copy) + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference") + } +} + +func TestReadOnlyList(t *testing.T) { + // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation + makeEntrySafeFn := func(value interface{}) interface{} { + return value.(string) + "_readonly" + } + + l := &list.List{} + l.Append("value1", "value2", "value3") + + roList := Wrap(l, makeEntrySafeFn) + + tests := []struct { + name string + index int + expected interface{} + }{ + {"ExistingIndex0", 0, "value1_readonly"}, + {"ExistingIndex1", 1, "value2_readonly"}, + {"NonExistingIndex", 3, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := roList.Get(tt.index) + if value != tt.expected { + t.Errorf("For index %d, expected %v, got %v", tt.index, tt.expected, value) + } + }) + } +} diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno index 3e093c4d0e0..17cb4e20ced 100644 --- a/examples/gno.land/p/demo/avl/rotree/rotree.gno +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -82,8 +82,23 @@ type ReadOnlyTree struct { makeEntrySafeFn func(interface{}) interface{} } -// Verify that ReadOnlyTree implements ITree -var _ avl.ITree = (*ReadOnlyTree)(nil) +// IReadOnlyTree defines the read-only operations available on a tree. +type IReadOnlyTree interface { + Size() int + Has(key string) bool + Get(key string) (interface{}, bool) + GetByIndex(index int) (string, interface{}) + Iterate(start, end string, cb avl.IterCbFn) bool + ReverseIterate(start, end string, cb avl.IterCbFn) bool + IterateByOffset(offset int, count int, cb avl.IterCbFn) bool + ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool +} + +// Verify that ReadOnlyTree implements both ITree and IReadOnlyTree +var ( + _ avl.ITree = (*ReadOnlyTree)(nil) + _ IReadOnlyTree = (*ReadOnlyTree)(nil) +) // getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { From cec48ecf3f35dd418a951c72072e2dbf2549abc7 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:53:09 +0100 Subject: [PATCH 08/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rolist/rolist.gno | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/avl/rolist/rolist.gno b/examples/gno.land/p/demo/avl/rolist/rolist.gno index d72f7465288..d7c6f56ac41 100644 --- a/examples/gno.land/p/demo/avl/rolist/rolist.gno +++ b/examples/gno.land/p/demo/avl/rolist/rolist.gno @@ -60,8 +60,9 @@ type ReadOnlyList struct { makeEntrySafeFn func(interface{}) interface{} } -// Verify that ReadOnlyList implements IReadOnlyList +// Verify interface implementations var _ IReadOnlyList = (*ReadOnlyList)(nil) +var _ list.IList = (interface{ IReadOnlyList })(nil) // is subset of list.IList // Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function. // If makeEntrySafeFn is nil, values will be returned as-is without transformation. From bdee5003b9da40fd6b2ed951dc0039513b11c06e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:56:08 +0100 Subject: [PATCH 09/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rolist/rolist.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/avl/rolist/rolist.gno b/examples/gno.land/p/demo/avl/rolist/rolist.gno index d7c6f56ac41..23a85d9c885 100644 --- a/examples/gno.land/p/demo/avl/rolist/rolist.gno +++ b/examples/gno.land/p/demo/avl/rolist/rolist.gno @@ -62,7 +62,7 @@ type ReadOnlyList struct { // Verify interface implementations var _ IReadOnlyList = (*ReadOnlyList)(nil) -var _ list.IList = (interface{ IReadOnlyList })(nil) // is subset of list.IList +var _ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList // Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function. // If makeEntrySafeFn is nil, values will be returned as-is without transformation. From 724cd6e8932336bd6646b1d22935caabc1b85073 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:04:32 +0100 Subject: [PATCH 10/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rolist/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/avl/rolist/gno.mod b/examples/gno.land/p/demo/avl/rolist/gno.mod index 6461f22c51c..682513c2cc3 100644 --- a/examples/gno.land/p/demo/avl/rolist/gno.mod +++ b/examples/gno.land/p/demo/avl/rolist/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/avl/rolist \ No newline at end of file +module gno.land/p/demo/avl/rolist From d96c765b3c73443f5d602d772f7793f7f74315e4 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:42:49 +0100 Subject: [PATCH 11/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao.gno | 80 ++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index 0e37453ac76..d262e7de21a 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -9,20 +9,39 @@ package udao import "time" +//----------------------------------------------------------------------------- +// DAO Interface +//----------------------------------------------------------------------------- + // DAO defines a minimal interface for a Decentralized Autonomous Organization type DAO interface { - // Propose submits a new proposal to the DAO - Propose(proposal PropDefinition) (uint64, error) + // Propose submits a new proposal to the DAO and returns the created Proposal + Propose(definition PropDefinition) (Proposal, error) - // GetProposal retrieves both the definition and status of a specific proposal - GetProposal(proposalID uint64) (PropDefinition, PropStatus, error) + // GetProposal retrieves a specific proposal + GetProposal(proposalID uint64) (Proposal, error) // Execute attempts to execute a proposal if it has passed Execute(proposalID uint64) error - // XXX: find a smart way to list proposals + // ActiveProposals returns a read-only list of active proposals + ActiveProposals() PropList + + // ArchivedProposals returns a read-only list of archived proposals + ArchivedProposals() PropList +} + +// Proposal represents a complete proposal including its ID, definition and status +type Proposal interface { + ID() uint64 + Definition() PropDefinition + PropStatus } +//----------------------------------------------------------------------------- +// Proposal Types & Helpers +//----------------------------------------------------------------------------- + // PropDefinition defines the interface for a DAO proposal type PropDefinition interface { Title() string @@ -64,10 +83,59 @@ type Constraint interface { // ValidateConstraints checks if all constraints of a proposal are met func ValidateConstraints(p Proposal) (bool, []string) { reasons := make([]string, 0) - for _, c := range p.Constraints() { + for _, c := range p.Definition().Constraints() { if valid, reason := c.Validate(); !valid { reasons = append(reasons, reason) } } return len(reasons) == 0, reasons } + +//----------------------------------------------------------------------------- +// PropList Interface & Implementation +//----------------------------------------------------------------------------- + +// PropList defines the read-only operations available on a proposal list +type PropList interface { + Len() int + Get(index int) Proposal + Slice(startIndex, endIndex int) []Proposal +} + +// PropListWrapper wraps an IReadOnlyList to implement PropList +type PropListWrapper struct { + list IReadOnlyList +} + +// WrapAsPropList converts an IReadOnlyList to a PropList +func WrapAsPropList(list IReadOnlyList) PropList { + return &PropListWrapper{list: list} +} + +// Len returns the number of proposals in the list +func (pl *PropListWrapper) Len() int { + return pl.list.Len() +} + +// Get returns the proposal and status at the specified index +func (pl *PropListWrapper) Get(index int) Proposal { + value := pl.list.Get(index) + if value == nil { + return nil + } + return value.(Proposal) +} + +// Slice returns a slice of proposals from startIndex to endIndex +func (pl *PropListWrapper) Slice(startIndex, endIndex int) []Proposal { + values := pl.list.Slice(startIndex, endIndex) + if values == nil { + return nil + } + + result := make([]Proposal, len(values)) + for i, v := range values { + result[i] = v.(Proposal) + } + return result +} From 8be3abe1d117cc9a2be5e5ec68692962dc70e96e Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:44:29 +0100 Subject: [PATCH 12/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao.gno | 10 +- examples/gno.land/p/moul/udao/udao_test.gno | 228 ++++++++++++++++++++ 2 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 examples/gno.land/p/moul/udao/udao_test.gno diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index d262e7de21a..259f15a3766 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -7,7 +7,11 @@ // and modular implementations of various DAO structures and behaviors. package udao -import "time" +import ( + "time" + + "gno.land/p/demo/avl/rolist" +) //----------------------------------------------------------------------------- // DAO Interface @@ -104,11 +108,11 @@ type PropList interface { // PropListWrapper wraps an IReadOnlyList to implement PropList type PropListWrapper struct { - list IReadOnlyList + list rolist.IReadOnlyList } // WrapAsPropList converts an IReadOnlyList to a PropList -func WrapAsPropList(list IReadOnlyList) PropList { +func WrapAsPropList(list rolist.IReadOnlyList) PropList { return &PropListWrapper{list: list} } diff --git a/examples/gno.land/p/moul/udao/udao_test.gno b/examples/gno.land/p/moul/udao/udao_test.gno new file mode 100644 index 00000000000..29f8c144a5c --- /dev/null +++ b/examples/gno.land/p/moul/udao/udao_test.gno @@ -0,0 +1,228 @@ +package udao + +import ( + "testing" + "time" +) + +// MockProposal implements the Proposal interface for testing +type MockProposal struct { + id uint64 + definition MockPropDefinition + PropStatus +} + +func (p MockProposal) ID() uint64 { return p.id } +func (p MockProposal) Definition() PropDefinition { return p.definition } + +// MockPropDefinition implements PropDefinition for testing +type MockPropDefinition struct { + title string + body string + created time.Time + constraints []Constraint +} + +func (d MockPropDefinition) Title() string { return d.title } +func (d MockPropDefinition) Body() string { return d.body } +func (d MockPropDefinition) Created() time.Time { return d.created } +func (d MockPropDefinition) Constraints() []Constraint { return d.constraints } + +// MockDAO implements the DAO interface for testing +type MockDAO struct { + proposals map[uint64]Proposal + nextID uint64 +} + +func NewMockDAO() *MockDAO { + return &MockDAO{ + proposals: make(map[uint64]Proposal), + nextID: 1, + } +} + +func (d *MockDAO) Propose(def PropDefinition) (Proposal, error) { + prop := MockProposal{ + id: d.nextID, + definition: def.(MockPropDefinition), + PropStatus: PropStatus{ + State: Pending, + YeaPercentage: 0, + NayPercentage: 0, + AbstainPercentage: 0, + PendingVoterPercentage: 100, + }, + } + d.proposals[d.nextID] = prop + d.nextID++ + return prop, nil +} + +func (d *MockDAO) GetProposal(proposalID uint64) (Proposal, error) { + if prop, ok := d.proposals[proposalID]; ok { + return prop, nil + } + return nil, nil +} + +func (d *MockDAO) Execute(proposalID uint64) error { + if prop, ok := d.proposals[proposalID]; ok { + if prop.(MockProposal).State == Passed { + newProp := prop.(MockProposal) + newProp.State = Executed + d.proposals[proposalID] = newProp + return nil + } + } + return nil +} + +func (d *MockDAO) ActiveProposals() PropList { + // Implementation omitted for brevity + return nil +} + +func (d *MockDAO) ArchivedProposals() PropList { + // Implementation omitted for brevity + return nil +} + +// Tests + +func TestPropose(t *testing.T) { + dao := NewMockDAO() + def := MockPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + } + + prop, err := dao.Propose(def) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if prop.ID() != 1 { + t.Errorf("Expected proposal ID 1, got %d", prop.ID()) + } + + if prop.Definition().Title() != "Test Proposal" { + t.Errorf("Expected title 'Test Proposal', got '%s'", prop.Definition().Title()) + } +} + +func TestGetProposal(t *testing.T) { + dao := NewMockDAO() + def := MockPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + } + + created, _ := dao.Propose(def) + retrieved, err := dao.GetProposal(created.ID()) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + if retrieved == nil { + t.Fatal("Expected to retrieve proposal, got nil") + } + + if retrieved.ID() != created.ID() { + t.Errorf("Expected proposal ID %d, got %d", created.ID(), retrieved.ID()) + } +} + +func TestExecuteProposal(t *testing.T) { + dao := NewMockDAO() + def := MockPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + } + + prop, _ := dao.Propose(def) + + // Manually set the proposal state to Passed + mockProp := prop.(MockProposal) + mockProp.State = Passed + dao.proposals[prop.ID()] = mockProp + + err := dao.Execute(prop.ID()) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + executed, _ := dao.GetProposal(prop.ID()) + if executed.(MockProposal).State != Executed { + t.Errorf("Expected proposal state to be Executed, got %s", executed.(MockProposal).State) + } +} + +// MockConstraint implements the Constraint interface for testing +type MockConstraint struct { + isValid bool + description string +} + +func (c MockConstraint) Validate() (bool, string) { + return c.isValid, c.description +} + +func TestValidateConstraints(t *testing.T) { + validConstraint := MockConstraint{isValid: true, description: "valid"} + invalidConstraint := MockConstraint{isValid: false, description: "invalid"} + + tests := []struct { + name string + constraints []Constraint + expectValid bool + expectedCount int + }{ + { + name: "No constraints", + constraints: []Constraint{}, + expectValid: true, + expectedCount: 0, + }, + { + name: "Single valid constraint", + constraints: []Constraint{validConstraint}, + expectValid: true, + expectedCount: 0, + }, + { + name: "Single invalid constraint", + constraints: []Constraint{invalidConstraint}, + expectValid: false, + expectedCount: 1, + }, + { + name: "Mixed constraints", + constraints: []Constraint{validConstraint, invalidConstraint}, + expectValid: false, + expectedCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prop := MockProposal{ + id: 1, + definition: MockPropDefinition{ + constraints: tt.constraints, + }, + } + + valid, reasons := ValidateConstraints(prop) + if valid != tt.expectValid { + t.Errorf("Expected valid=%v, got %v", tt.expectValid, valid) + } + if len(reasons) != tt.expectedCount { + t.Errorf("Expected %d reasons, got %d", tt.expectedCount, len(reasons)) + } + }) + } +} From 5de04405e8b735ffba2339ab07206a7eebc6d091 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:50:05 +0100 Subject: [PATCH 13/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao.gno | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index 259f15a3766..965bdfa139b 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -14,11 +14,12 @@ import ( ) //----------------------------------------------------------------------------- -// DAO Interface +// DAO Interfaces //----------------------------------------------------------------------------- -// DAO defines a minimal interface for a Decentralized Autonomous Organization -type DAO interface { +// LightDAO defines a minimal interface for a Decentralized Autonomous Organization +// without proposal listing capabilities +type LightDAO interface { // Propose submits a new proposal to the DAO and returns the created Proposal Propose(definition PropDefinition) (Proposal, error) @@ -28,6 +29,14 @@ type DAO interface { // Execute attempts to execute a proposal if it has passed Execute(proposalID uint64) error + // Len returns the total number of proposals + Len() int +} + +// DAO extends LightDAO with proposal listing capabilities +type DAO interface { + LightDAO + // ActiveProposals returns a read-only list of active proposals ActiveProposals() PropList From 5021cbc3d998f501cf078a0e82355c6394f84c48 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:25:48 +0100 Subject: [PATCH 14/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/types.gno | 57 +++++ examples/gno.land/p/moul/udao/udao.gno | 220 ++++++++--------- examples/gno.land/p/moul/udao/udao_test.gno | 259 ++++++-------------- examples/gno.land/p/moul/udao/wrappers.gno | 32 +++ 4 files changed, 263 insertions(+), 305 deletions(-) create mode 100644 examples/gno.land/p/moul/udao/types.gno create mode 100644 examples/gno.land/p/moul/udao/wrappers.gno diff --git a/examples/gno.land/p/moul/udao/types.gno b/examples/gno.land/p/moul/udao/types.gno new file mode 100644 index 00000000000..9543df9789e --- /dev/null +++ b/examples/gno.land/p/moul/udao/types.gno @@ -0,0 +1,57 @@ +package udao + +import ( + "time" +) + +// DAO defines the interface for proposal management +type DAO interface { + // Core proposal operations + Propose(def PropDefinition) (Proposal, error) + GetProposal(proposalID uint64) (Proposal, error) + Execute(proposalID uint64) error + + // List operations + ActiveProposals() PropList + ArchivedProposals() PropList + Len() int +} + +// PropDefinition defines the content of a proposal +type PropDefinition interface { + Title() string + Body() string + Created() time.Time + Finished() time.Time + CheckConstraints() (valid bool, reasons []string) +} + +// Proposal represents a complete proposal including its ID, definition and status +type Proposal interface { + ID() uint64 + Definition() PropDefinition + GetState() PropState + GetYeaPercentage() float64 + GetNayPercentage() float64 + GetAbstainPercentage() float64 + GetPendingVoterPercentage() float64 +} + +// PropState represents the state of a proposal +type PropState string + +const ( + Pending PropState = "pending" + Active PropState = "active" + Passed PropState = "passed" + Failed PropState = "failed" + Executed PropState = "executed" + Cancelled PropState = "cancelled" +) + +// PropList defines the read-only operations available on a proposal list +type PropList interface { + Len() int + Get(index int) Proposal + Slice(startIndex, endIndex int) []Proposal +} diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index 965bdfa139b..c707d9a0d53 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -1,154 +1,140 @@ -// Package udao defines minimal interfaces for Decentralized Autonomous Organizations (DAOs). -// It intentionally does not expose members and votes, as these details are implementation-specific. -// Instead, it focuses on providing an external view of proposals and their statuses, -// which is what non-members and non-voters typically care about. -// -// The package is designed to allow for composable DAO patterns, enabling flexible -// and modular implementations of various DAO structures and behaviors. package udao import ( "time" - "gno.land/p/demo/avl/rolist" + "gno.land/p/demo/avl/list" ) -//----------------------------------------------------------------------------- -// DAO Interfaces -//----------------------------------------------------------------------------- - -// LightDAO defines a minimal interface for a Decentralized Autonomous Organization -// without proposal listing capabilities -type LightDAO interface { - // Propose submits a new proposal to the DAO and returns the created Proposal - Propose(definition PropDefinition) (Proposal, error) - - // GetProposal retrieves a specific proposal - GetProposal(proposalID uint64) (Proposal, error) - - // Execute attempts to execute a proposal if it has passed - Execute(proposalID uint64) error - - // Len returns the total number of proposals - Len() int +// BasicProposal implements the Proposal interface +type BasicProposal struct { + id uint64 + definition PropDefinition + state PropState + yea float64 + nay float64 + abstain float64 + pending float64 } -// DAO extends LightDAO with proposal listing capabilities -type DAO interface { - LightDAO - - // ActiveProposals returns a read-only list of active proposals - ActiveProposals() PropList - - // ArchivedProposals returns a read-only list of archived proposals - ArchivedProposals() PropList +func (p BasicProposal) ID() uint64 { return p.id } +func (p BasicProposal) Definition() PropDefinition { return p.definition } +func (p BasicProposal) GetState() PropState { return p.state } +func (p BasicProposal) GetYeaPercentage() float64 { return p.yea } +func (p BasicProposal) GetNayPercentage() float64 { return p.nay } +func (p BasicProposal) GetAbstainPercentage() float64 { return p.abstain } +func (p BasicProposal) GetPendingVoterPercentage() float64 { return p.pending } + +// BasicPropDefinition implements PropDefinition +type BasicPropDefinition struct { + title string + body string + created time.Time + finished time.Time } -// Proposal represents a complete proposal including its ID, definition and status -type Proposal interface { - ID() uint64 - Definition() PropDefinition - PropStatus +func (d BasicPropDefinition) Title() string { return d.title } +func (d BasicPropDefinition) Body() string { return d.body } +func (d BasicPropDefinition) Created() time.Time { return d.created } +func (d BasicPropDefinition) Finished() time.Time { return d.finished } +func (d BasicPropDefinition) CheckConstraints() (valid bool, reasons []string) { + panic("not implemented") } -//----------------------------------------------------------------------------- -// Proposal Types & Helpers -//----------------------------------------------------------------------------- - -// PropDefinition defines the interface for a DAO proposal -type PropDefinition interface { - Title() string - Body() string - Created() time.Time - Constraints() []Constraint +// BasicDAO provides a minimal implementation of the DAO interface +type BasicDAO struct { + proposals map[uint64]Proposal + nextID uint64 + active *list.List + archived *list.List } -// PropStatus represents the current status and metrics of a proposal -type PropStatus struct { - // status - State PropState - - // metrics - YeaPercentage float64 - NayPercentage float64 - AbstainPercentage float64 - PendingVoterPercentage float64 - // XXX: other metrics? +// NewBasicDAO creates a new BasicDAO instance +func NewBasicDAO() *BasicDAO { + return &BasicDAO{ + proposals: make(map[uint64]Proposal), + nextID: 1, + active: &list.List{}, + archived: &list.List{}, + } } -// PropState represents the current state of a proposal -type PropState string - -const ( - Pending PropState = "pending" - Active PropState = "active" - Passed PropState = "passed" - Rejected PropState = "rejected" - Executed PropState = "executed" - Expired PropState = "expired" -) - -// Constraint defines an interface for proposal constraints -type Constraint interface { - Validate() (valid bool, description string) +func (d *BasicDAO) Propose(def PropDefinition) (Proposal, error) { + prop := BasicProposal{ + id: d.nextID, + definition: def, + state: Pending, + yea: 0, + nay: 0, + abstain: 0, + pending: 100, + } + d.proposals[d.nextID] = prop + d.active.Append(prop) + d.nextID++ + return prop, nil } -// ValidateConstraints checks if all constraints of a proposal are met -func ValidateConstraints(p Proposal) (bool, []string) { - reasons := make([]string, 0) - for _, c := range p.Definition().Constraints() { - if valid, reason := c.Validate(); !valid { - reasons = append(reasons, reason) - } +func (d *BasicDAO) GetProposal(proposalID uint64) (Proposal, error) { + if prop, ok := d.proposals[proposalID]; ok { + return prop, nil } - return len(reasons) == 0, reasons + return nil, nil } -//----------------------------------------------------------------------------- -// PropList Interface & Implementation -//----------------------------------------------------------------------------- - -// PropList defines the read-only operations available on a proposal list -type PropList interface { - Len() int - Get(index int) Proposal - Slice(startIndex, endIndex int) []Proposal +func (d *BasicDAO) Execute(proposalID uint64) error { + if prop, ok := d.proposals[proposalID]; ok { + if prop.GetState() == Passed { + newProp := prop.(BasicProposal) + newProp.state = Executed + d.proposals[proposalID] = newProp + + // Move from active to archived + for i := 0; i < d.active.Len(); i++ { + if p := d.active.Get(i).(Proposal); p.ID() == proposalID { + d.active.Delete(i) + d.archived.Append(newProp) + break + } + } + return nil + } + } + return nil } -// PropListWrapper wraps an IReadOnlyList to implement PropList -type PropListWrapper struct { - list rolist.IReadOnlyList +func (d *BasicDAO) ActiveProposals() PropList { + return WrapAsPropList(d.active) } -// WrapAsPropList converts an IReadOnlyList to a PropList -func WrapAsPropList(list rolist.IReadOnlyList) PropList { - return &PropListWrapper{list: list} +func (d *BasicDAO) ArchivedProposals() PropList { + return WrapAsPropList(d.archived) } -// Len returns the number of proposals in the list -func (pl *PropListWrapper) Len() int { - return pl.list.Len() +func (d *BasicDAO) Len() int { + return len(d.proposals) } -// Get returns the proposal and status at the specified index -func (pl *PropListWrapper) Get(index int) Proposal { - value := pl.list.Get(index) - if value == nil { +// Helper methods for managing proposal state +func (d *BasicDAO) UpdateProposalState(proposalID uint64, state PropState) error { + if prop, ok := d.proposals[proposalID]; ok { + newProp := prop.(BasicProposal) + newProp.state = state + d.proposals[proposalID] = newProp return nil } - return value.(Proposal) + return nil } -// Slice returns a slice of proposals from startIndex to endIndex -func (pl *PropListWrapper) Slice(startIndex, endIndex int) []Proposal { - values := pl.list.Slice(startIndex, endIndex) - if values == nil { +func (d *BasicDAO) UpdateVotingPercentages(proposalID uint64, yea, nay, abstain, pending float64) error { + if prop, ok := d.proposals[proposalID]; ok { + newProp := prop.(BasicProposal) + newProp.yea = yea + newProp.nay = nay + newProp.abstain = abstain + newProp.pending = pending + d.proposals[proposalID] = newProp return nil } - - result := make([]Proposal, len(values)) - for i, v := range values { - result[i] = v.(Proposal) - } - return result + return nil } diff --git a/examples/gno.land/p/moul/udao/udao_test.gno b/examples/gno.land/p/moul/udao/udao_test.gno index 29f8c144a5c..66c91316494 100644 --- a/examples/gno.land/p/moul/udao/udao_test.gno +++ b/examples/gno.land/p/moul/udao/udao_test.gno @@ -5,224 +5,107 @@ import ( "time" ) -// MockProposal implements the Proposal interface for testing -type MockProposal struct { - id uint64 - definition MockPropDefinition - PropStatus -} - -func (p MockProposal) ID() uint64 { return p.id } -func (p MockProposal) Definition() PropDefinition { return p.definition } - -// MockPropDefinition implements PropDefinition for testing -type MockPropDefinition struct { - title string - body string - created time.Time - constraints []Constraint -} - -func (d MockPropDefinition) Title() string { return d.title } -func (d MockPropDefinition) Body() string { return d.body } -func (d MockPropDefinition) Created() time.Time { return d.created } -func (d MockPropDefinition) Constraints() []Constraint { return d.constraints } - -// MockDAO implements the DAO interface for testing -type MockDAO struct { - proposals map[uint64]Proposal - nextID uint64 -} - -func NewMockDAO() *MockDAO { - return &MockDAO{ - proposals: make(map[uint64]Proposal), - nextID: 1, +func TestBasicDAO(t *testing.T) { + dao := NewBasicDAO() + if dao.Len() != 0 { + t.Errorf("expected empty DAO, got len=%d", dao.Len()) } -} - -func (d *MockDAO) Propose(def PropDefinition) (Proposal, error) { - prop := MockProposal{ - id: d.nextID, - definition: def.(MockPropDefinition), - PropStatus: PropStatus{ - State: Pending, - YeaPercentage: 0, - NayPercentage: 0, - AbstainPercentage: 0, - PendingVoterPercentage: 100, - }, - } - d.proposals[d.nextID] = prop - d.nextID++ - return prop, nil -} - -func (d *MockDAO) GetProposal(proposalID uint64) (Proposal, error) { - if prop, ok := d.proposals[proposalID]; ok { - return prop, nil - } - return nil, nil -} - -func (d *MockDAO) Execute(proposalID uint64) error { - if prop, ok := d.proposals[proposalID]; ok { - if prop.(MockProposal).State == Passed { - newProp := prop.(MockProposal) - newProp.State = Executed - d.proposals[proposalID] = newProp - return nil - } - } - return nil -} - -func (d *MockDAO) ActiveProposals() PropList { - // Implementation omitted for brevity - return nil -} - -func (d *MockDAO) ArchivedProposals() PropList { - // Implementation omitted for brevity - return nil -} - -// Tests -func TestPropose(t *testing.T) { - dao := NewMockDAO() - def := MockPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), + // Test proposal creation + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), } prop, err := dao.Propose(def) if err != nil { - t.Errorf("Unexpected error: %v", err) + t.Errorf("unexpected error creating proposal: %v", err) } - if prop.ID() != 1 { - t.Errorf("Expected proposal ID 1, got %d", prop.ID()) - } - - if prop.Definition().Title() != "Test Proposal" { - t.Errorf("Expected title 'Test Proposal', got '%s'", prop.Definition().Title()) + t.Errorf("expected proposal ID=1, got %d", prop.ID()) } -} - -func TestGetProposal(t *testing.T) { - dao := NewMockDAO() - def := MockPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), + if dao.Len() != 1 { + t.Errorf("expected DAO len=1, got %d", dao.Len()) } - created, _ := dao.Propose(def) - retrieved, err := dao.GetProposal(created.ID()) - + // Test proposal retrieval + retrieved, err := dao.GetProposal(1) if err != nil { - t.Errorf("Unexpected error: %v", err) + t.Errorf("unexpected error getting proposal: %v", err) } - if retrieved == nil { - t.Fatal("Expected to retrieve proposal, got nil") + t.Fatal("expected to retrieve proposal, got nil") } - - if retrieved.ID() != created.ID() { - t.Errorf("Expected proposal ID %d, got %d", created.ID(), retrieved.ID()) + if retrieved.ID() != prop.ID() { + t.Errorf("expected retrieved ID=%d, got %d", prop.ID(), retrieved.ID()) } -} -func TestExecuteProposal(t *testing.T) { - dao := NewMockDAO() - def := MockPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), + // Test active proposals list + active := dao.ActiveProposals() + if active.Len() != 1 { + t.Errorf("expected 1 active proposal, got %d", active.Len()) + } + if active.Get(0).ID() != prop.ID() { + t.Errorf("expected active proposal ID=%d, got %d", prop.ID(), active.Get(0).ID()) } - prop, _ := dao.Propose(def) - - // Manually set the proposal state to Passed - mockProp := prop.(MockProposal) - mockProp.State = Passed - dao.proposals[prop.ID()] = mockProp - - err := dao.Execute(prop.ID()) + // Test proposal execution + dao.UpdateProposalState(1, Passed) + err = dao.Execute(1) if err != nil { - t.Errorf("Unexpected error: %v", err) + t.Errorf("unexpected error executing proposal: %v", err) } - executed, _ := dao.GetProposal(prop.ID()) - if executed.(MockProposal).State != Executed { - t.Errorf("Expected proposal state to be Executed, got %s", executed.(MockProposal).State) + executed, _ := dao.GetProposal(1) + if executed.GetState() != Executed { + t.Errorf("expected state=Executed, got %s", executed.GetState()) } -} -// MockConstraint implements the Constraint interface for testing -type MockConstraint struct { - isValid bool - description string + // Test archived proposals + archived := dao.ArchivedProposals() + if archived.Len() != 1 { + t.Errorf("expected 1 archived proposal, got %d", archived.Len()) + } + if archived.Get(0).ID() != prop.ID() { + t.Errorf("expected archived proposal ID=%d, got %d", prop.ID(), archived.Get(0).ID()) + } } -func (c MockConstraint) Validate() (bool, string) { - return c.isValid, c.description -} +func TestPropListWrapper(t *testing.T) { + dao := NewBasicDAO() -func TestValidateConstraints(t *testing.T) { - validConstraint := MockConstraint{isValid: true, description: "valid"} - invalidConstraint := MockConstraint{isValid: false, description: "invalid"} + // Create multiple proposals + for i := 0; i < 3; i++ { + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + dao.Propose(def) + } + + active := dao.ActiveProposals() - tests := []struct { - name string - constraints []Constraint - expectValid bool - expectedCount int - }{ - { - name: "No constraints", - constraints: []Constraint{}, - expectValid: true, - expectedCount: 0, - }, - { - name: "Single valid constraint", - constraints: []Constraint{validConstraint}, - expectValid: true, - expectedCount: 0, - }, - { - name: "Single invalid constraint", - constraints: []Constraint{invalidConstraint}, - expectValid: false, - expectedCount: 1, - }, - { - name: "Mixed constraints", - constraints: []Constraint{validConstraint, invalidConstraint}, - expectValid: false, - expectedCount: 1, - }, + // Test Len() + if active.Len() != 3 { + t.Errorf("expected len=3, got %d", active.Len()) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - prop := MockProposal{ - id: 1, - definition: MockPropDefinition{ - constraints: tt.constraints, - }, - } + // Test Get() + prop := active.Get(0) + if prop.ID() != 1 { + t.Errorf("expected first proposal ID=1, got %d", prop.ID()) + } - valid, reasons := ValidateConstraints(prop) - if valid != tt.expectValid { - t.Errorf("Expected valid=%v, got %v", tt.expectValid, valid) - } - if len(reasons) != tt.expectedCount { - t.Errorf("Expected %d reasons, got %d", tt.expectedCount, len(reasons)) - } - }) + // Test Slice() + slice := active.Slice(0, 2) + if len(slice) != 2 { + t.Errorf("expected slice len=2, got %d", len(slice)) + } + if slice[0].ID() != 1 || slice[1].ID() != 2 { + t.Errorf("unexpected slice IDs: %d, %d", slice[0].ID(), slice[1].ID()) } } diff --git a/examples/gno.land/p/moul/udao/wrappers.gno b/examples/gno.land/p/moul/udao/wrappers.gno new file mode 100644 index 00000000000..ea06e439bd0 --- /dev/null +++ b/examples/gno.land/p/moul/udao/wrappers.gno @@ -0,0 +1,32 @@ +package udao + +import ( + "gno.land/p/demo/avl/list" +) + +// PropListWrapper wraps an IReadOnlyList to implement PropList +type PropListWrapper struct { + list list.IList +} + +// WrapAsPropList converts an IReadOnlyList to a PropList +func WrapAsPropList(list list.IList) PropList { + return &PropListWrapper{list: list} +} + +func (pl *PropListWrapper) Len() int { + return pl.list.Len() +} + +func (pl *PropListWrapper) Get(index int) Proposal { + return pl.list.Get(index).(Proposal) +} + +func (pl *PropListWrapper) Slice(startIndex, endIndex int) []Proposal { + slice := pl.list.Slice(startIndex, endIndex) + result := make([]Proposal, len(slice)) + for i, v := range slice { + result[i] = v.(Proposal) + } + return result +} From 2f7cfd8b03c03b7f696c027e3d6dd8b8f2ef147a Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:26:38 +0100 Subject: [PATCH 15/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/udao/udao_test.gno | 162 ++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/examples/gno.land/p/moul/udao/udao_test.gno b/examples/gno.land/p/moul/udao/udao_test.gno index 66c91316494..8029bca033e 100644 --- a/examples/gno.land/p/moul/udao/udao_test.gno +++ b/examples/gno.land/p/moul/udao/udao_test.gno @@ -109,3 +109,165 @@ func TestPropListWrapper(t *testing.T) { t.Errorf("unexpected slice IDs: %d, %d", slice[0].ID(), slice[1].ID()) } } + +func TestProposalVoting(t *testing.T) { + dao := NewBasicDAO() + + // Create a proposal + def := BasicPropDefinition{ + title: "Voting Test", + body: "Testing voting percentages", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ := dao.Propose(def) + + // Test initial voting percentages + if prop.GetYeaPercentage() != 0 { + t.Errorf("expected initial yea=0, got %f", prop.GetYeaPercentage()) + } + if prop.GetNayPercentage() != 0 { + t.Errorf("expected initial nay=0, got %f", prop.GetNayPercentage()) + } + if prop.GetPendingVoterPercentage() != 100 { + t.Errorf("expected initial pending=100, got %f", prop.GetPendingVoterPercentage()) + } + + // Update voting percentages + err := dao.UpdateVotingPercentages(prop.ID(), 60, 30, 10, 0) + if err != nil { + t.Errorf("unexpected error updating voting percentages: %v", err) + } + + // Verify updated percentages + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetYeaPercentage() != 60 { + t.Errorf("expected yea=60, got %f", updated.GetYeaPercentage()) + } + if updated.GetNayPercentage() != 30 { + t.Errorf("expected nay=30, got %f", updated.GetNayPercentage()) + } + if updated.GetAbstainPercentage() != 10 { + t.Errorf("expected abstain=10, got %f", updated.GetAbstainPercentage()) + } + if updated.GetPendingVoterPercentage() != 0 { + t.Errorf("expected pending=0, got %f", updated.GetPendingVoterPercentage()) + } +} + +func TestProposalStateTransitions(t *testing.T) { + dao := NewBasicDAO() + + // Create a proposal + def := BasicPropDefinition{ + title: "State Test", + body: "Testing state transitions", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ := dao.Propose(def) + + // Test initial state + if prop.GetState() != Pending { + t.Errorf("expected initial state=Pending, got %s", prop.GetState()) + } + + // Test state transitions + states := []PropState{Active, Passed, Failed, Executed, Cancelled} + for _, state := range states { + err := dao.UpdateProposalState(prop.ID(), state) + if err != nil { + t.Errorf("unexpected error updating state to %s: %v", state, err) + } + + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetState() != state { + t.Errorf("expected state=%s, got %s", state, updated.GetState()) + } + } +} + +func TestInvalidProposalOperations(t *testing.T) { + dao := NewBasicDAO() + + // Test getting non-existent proposal + prop, err := dao.GetProposal(999) + if err != nil { + t.Errorf("expected nil error for non-existent proposal, got %v", err) + } + if prop != nil { + t.Error("expected nil proposal for non-existent ID") + } + + // Test executing non-existent proposal + err = dao.Execute(999) + if err != nil { + t.Errorf("expected nil error for executing non-existent proposal, got %v", err) + } + + // Test executing non-passed proposal + def := BasicPropDefinition{ + title: "Invalid Execute", + body: "Testing invalid execution", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ = dao.Propose(def) + err = dao.Execute(prop.ID()) + if err != nil { + t.Errorf("expected nil error for executing non-passed proposal, got %v", err) + } + + // Verify state didn't change + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetState() != Pending { + t.Errorf("expected state to remain Pending, got %s", updated.GetState()) + } +} + +func TestPropListSlicing(t *testing.T) { + dao := NewBasicDAO() + + // Create 5 proposals + for i := 0; i < 5; i++ { + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + dao.Propose(def) + } + + active := dao.ActiveProposals() + + // Test various slice operations + testCases := []struct { + start int + end int + expected int + }{ + {0, 2, 2}, + {1, 4, 3}, + {0, 5, 5}, + {2, 3, 1}, + {4, 5, 1}, + } + + for _, tc := range testCases { + slice := active.Slice(tc.start, tc.end) + if len(slice) != tc.expected { + t.Errorf("expected slice[%d:%d] len=%d, got %d", + tc.start, tc.end, tc.expected, len(slice)) + } + + // Verify IDs are sequential + for i := range slice { + expectedID := uint64(tc.start + i + 1) + if slice[i].ID() != expectedID { + t.Errorf("expected ID=%d at index %d, got %d", + expectedID, i, slice[i].ID()) + } + } + } +} From 4d82dfb3d4cc257a6ab2ae7b413cb36304e73388 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:07:42 +0000 Subject: [PATCH 16/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rolist/gno.mod | 1 - .../gno.land/p/demo/avl/rolist/rolist.gno | 119 ------------- .../p/demo/avl/rolist/rolist_test.gno | 162 ------------------ 3 files changed, 282 deletions(-) delete mode 100644 examples/gno.land/p/demo/avl/rolist/gno.mod delete mode 100644 examples/gno.land/p/demo/avl/rolist/rolist.gno delete mode 100644 examples/gno.land/p/demo/avl/rolist/rolist_test.gno diff --git a/examples/gno.land/p/demo/avl/rolist/gno.mod b/examples/gno.land/p/demo/avl/rolist/gno.mod deleted file mode 100644 index 682513c2cc3..00000000000 --- a/examples/gno.land/p/demo/avl/rolist/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/avl/rolist diff --git a/examples/gno.land/p/demo/avl/rolist/rolist.gno b/examples/gno.land/p/demo/avl/rolist/rolist.gno deleted file mode 100644 index 23a85d9c885..00000000000 --- a/examples/gno.land/p/demo/avl/rolist/rolist.gno +++ /dev/null @@ -1,119 +0,0 @@ -// Package rolist provides a read-only wrapper for list.List with safe value transformation. -// -// It is useful when you want to expose a read-only view of a list while ensuring that -// the sensitive data cannot be modified. -// -// Example: -// -// // Define a user structure with sensitive data -// type User struct { -// Name string -// Balance int -// Internal string // sensitive field -// } -// -// // Create and populate the original list -// privateList := list.New() -// privateList.Append(&User{ -// Name: "Alice", -// Balance: 100, -// Internal: "sensitive", -// }) -// -// // Create a safe transformation function that copies the struct -// // while excluding sensitive data -// makeEntrySafeFn := func(v interface{}) interface{} { -// u := v.(*User) -// return &User{ -// Name: u.Name, -// Balance: u.Balance, -// Internal: "", // omit sensitive data -// } -// } -// -// // Create a read-only view of the list -// publicList := rolist.Wrap(list, makeEntrySafeFn) -// -// // Safely access the data -// value := publicList.Get(0) -// user := value.(*User) -// // user.Name == "Alice" -// // user.Balance == 100 -// // user.Internal == "" (sensitive data is filtered) -package rolist - -import ( - "gno.land/p/demo/avl/list" -) - -// IReadOnlyList defines the read-only operations available on a list. -type IReadOnlyList interface { - Len() int - Get(index int) interface{} - Slice(startIndex, endIndex int) []interface{} - ForEach(fn func(index int, value interface{}) bool) -} - -// ReadOnlyList wraps a list.List and provides read-only access. -type ReadOnlyList struct { - list *list.List - makeEntrySafeFn func(interface{}) interface{} -} - -// Verify interface implementations -var _ IReadOnlyList = (*ReadOnlyList)(nil) -var _ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList - -// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function. -// If makeEntrySafeFn is nil, values will be returned as-is without transformation. -func Wrap(list *list.List, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyList { - return &ReadOnlyList{ - list: list, - makeEntrySafeFn: makeEntrySafeFn, - } -} - -// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value -func (rol *ReadOnlyList) getSafeValue(value interface{}) interface{} { - if rol.makeEntrySafeFn == nil { - return value - } - return rol.makeEntrySafeFn(value) -} - -// Len returns the number of elements in the list. -func (rol *ReadOnlyList) Len() int { - return rol.list.Len() -} - -// Get returns the value at the specified index, converted to a safe format. -// Returns nil if index is out of bounds. -func (rol *ReadOnlyList) Get(index int) interface{} { - value := rol.list.Get(index) - if value == nil { - return nil - } - return rol.getSafeValue(value) -} - -// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive), -// with all values converted to a safe format. -func (rol *ReadOnlyList) Slice(startIndex, endIndex int) []interface{} { - values := rol.list.Slice(startIndex, endIndex) - if values == nil { - return nil - } - - result := make([]interface{}, len(values)) - for i, v := range values { - result[i] = rol.getSafeValue(v) - } - return result -} - -// ForEach iterates through all elements in the list, providing safe versions of the values. -func (rol *ReadOnlyList) ForEach(fn func(index int, value interface{}) bool) { - rol.list.ForEach(func(index int, value interface{}) bool { - return fn(index, rol.getSafeValue(value)) - }) -} diff --git a/examples/gno.land/p/demo/avl/rolist/rolist_test.gno b/examples/gno.land/p/demo/avl/rolist/rolist_test.gno deleted file mode 100644 index 03b0a8cba30..00000000000 --- a/examples/gno.land/p/demo/avl/rolist/rolist_test.gno +++ /dev/null @@ -1,162 +0,0 @@ -package rolist - -import ( - "testing" - - "gno.land/p/demo/avl/list" -) - -func TestExample(t *testing.T) { - // User represents our internal data structure - type User struct { - ID string - Name string - Balance int - Internal string // sensitive internal data - } - - // Create and populate the original list - l := &list.List{} - l.Append( - &User{ - ID: "1", - Name: "Alice", - Balance: 100, - Internal: "sensitive_data_1", - }, - &User{ - ID: "2", - Name: "Bob", - Balance: 200, - Internal: "sensitive_data_2", - }, - ) - - // Define a makeEntrySafeFn that: - // 1. Creates a defensive copy of the User struct - // 2. Omits sensitive internal data - makeEntrySafeFn := func(v interface{}) interface{} { - originalUser := v.(*User) - return &User{ - ID: originalUser.ID, - Name: originalUser.Name, - Balance: originalUser.Balance, - Internal: "", // Omit sensitive data - } - } - - // Create a read-only view of the list - roList := Wrap(l, makeEntrySafeFn) - - // Test retrieving and verifying a user - t.Run("Get User", func(t *testing.T) { - // Get user from read-only list - value := roList.Get(0) - if value == nil { - t.Fatal("User at index 0 not found") - } - - user := value.(*User) - - // Verify user data is correct - if user.Name != "Alice" || user.Balance != 100 { - t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) - } - - // Verify sensitive data is not exposed - if user.Internal != "" { - t.Error("Sensitive data should not be exposed") - } - - // Verify it's a different instance than the original - originalUser := l.Get(0).(*User) - if user == originalUser { - t.Error("Read-only list should return a copy, not the original pointer") - } - }) - - // Test slice functionality - t.Run("Slice Users", func(t *testing.T) { - users := roList.Slice(0, 2) - if len(users) != 2 { - t.Fatalf("Expected 2 users, got %d", len(users)) - } - - for _, v := range users { - user := v.(*User) - if user.Internal != "" { - t.Error("Sensitive data exposed in slice") - } - } - }) - - // Test ForEach functionality - t.Run("ForEach Users", func(t *testing.T) { - count := 0 - roList.ForEach(func(index int, value interface{}) bool { - user := value.(*User) - if user.Internal != "" { - t.Error("Sensitive data exposed during iteration") - } - count++ - return false - }) - - if count != 2 { - t.Errorf("Expected 2 users, got %d", count) - } - }) -} - -func TestNilMakeEntrySafeFn(t *testing.T) { - // Create a list with some test data - l := &list.List{} - originalValue := []int{1, 2, 3} - l.Append(originalValue) - - // Create a ReadOnlyList with nil makeEntrySafeFn - roList := Wrap(l, nil) - - // Test that we get back the original value - value := roList.Get(0) - if value == nil { - t.Fatal("Value not found") - } - - // Verify it's the exact same slice (not a copy) - retrievedSlice := value.([]int) - if &retrievedSlice[0] != &originalValue[0] { - t.Error("Expected to get back the original slice reference") - } -} - -func TestReadOnlyList(t *testing.T) { - // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation - makeEntrySafeFn := func(value interface{}) interface{} { - return value.(string) + "_readonly" - } - - l := &list.List{} - l.Append("value1", "value2", "value3") - - roList := Wrap(l, makeEntrySafeFn) - - tests := []struct { - name string - index int - expected interface{} - }{ - {"ExistingIndex0", 0, "value1_readonly"}, - {"ExistingIndex1", 1, "value2_readonly"}, - {"NonExistingIndex", 3, nil}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - value := roList.Get(tt.index) - if value != tt.expected { - t.Errorf("For index %d, expected %v, got %v", tt.index, tt.expected, value) - } - }) - } -} From e3a1b1ad0b19bfa9d8d5b48b74882a51e2c0f158 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Thu, 9 Jan 2025 22:08:49 +0000 Subject: [PATCH 17/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/demo/avl/rotree/rotree.gno | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno index 17cb4e20ced..3e093c4d0e0 100644 --- a/examples/gno.land/p/demo/avl/rotree/rotree.gno +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -82,23 +82,8 @@ type ReadOnlyTree struct { makeEntrySafeFn func(interface{}) interface{} } -// IReadOnlyTree defines the read-only operations available on a tree. -type IReadOnlyTree interface { - Size() int - Has(key string) bool - Get(key string) (interface{}, bool) - GetByIndex(index int) (string, interface{}) - Iterate(start, end string, cb avl.IterCbFn) bool - ReverseIterate(start, end string, cb avl.IterCbFn) bool - IterateByOffset(offset int, count int, cb avl.IterCbFn) bool - ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool -} - -// Verify that ReadOnlyTree implements both ITree and IReadOnlyTree -var ( - _ avl.ITree = (*ReadOnlyTree)(nil) - _ IReadOnlyTree = (*ReadOnlyTree)(nil) -) +// Verify that ReadOnlyTree implements ITree +var _ avl.ITree = (*ReadOnlyTree)(nil) // getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { From 8ac6d525041195271abfbd611f597edf98041f17 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:59:23 +0000 Subject: [PATCH 18/18] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../p/moul/udao/basicdao/basicdao.gno | 142 +++++++++ .../p/moul/udao/basicdao/basicdao_test.gno | 276 ++++++++++++++++++ .../gno.land/p/moul/udao/basicdao/gno.mod | 1 + examples/gno.land/p/moul/udao/types.gno | 57 ---- examples/gno.land/p/moul/udao/udao.gno | 185 ++++-------- examples/gno.land/p/moul/udao/udao_test.gno | 272 ----------------- .../udao/{wrappers.gno => wrap/avl_list.gno} | 15 +- .../p/moul/udao/wrap/avl_list_test.gno | 1 + examples/gno.land/p/moul/udao/wrap/gno.mod | 1 + 9 files changed, 479 insertions(+), 471 deletions(-) create mode 100644 examples/gno.land/p/moul/udao/basicdao/basicdao.gno create mode 100644 examples/gno.land/p/moul/udao/basicdao/basicdao_test.gno create mode 100644 examples/gno.land/p/moul/udao/basicdao/gno.mod delete mode 100644 examples/gno.land/p/moul/udao/types.gno rename examples/gno.land/p/moul/udao/{wrappers.gno => wrap/avl_list.gno} (54%) create mode 100644 examples/gno.land/p/moul/udao/wrap/avl_list_test.gno create mode 100644 examples/gno.land/p/moul/udao/wrap/gno.mod diff --git a/examples/gno.land/p/moul/udao/basicdao/basicdao.gno b/examples/gno.land/p/moul/udao/basicdao/basicdao.gno new file mode 100644 index 00000000000..991631ce7fb --- /dev/null +++ b/examples/gno.land/p/moul/udao/basicdao/basicdao.gno @@ -0,0 +1,142 @@ +package basicdao + +import ( + "time" + + "gno.land/p/demo/avl/list" + "gno.land/p/moul/udao" + "gno.land/p/moul/udao/wrap" +) + +// BasicProposal implements the Proposal interface +type BasicProposal struct { + id uint64 + definition udao.PropDefinition + state udao.PropState + yea float64 + nay float64 + abstain float64 + pending float64 +} + +func (p BasicProposal) ID() uint64 { return p.id } +func (p BasicProposal) Definition() udao.PropDefinition { return p.definition } +func (p BasicProposal) GetState() udao.PropState { return p.state } +func (p BasicProposal) GetYeaPercentage() float64 { return p.yea } +func (p BasicProposal) GetNayPercentage() float64 { return p.nay } +func (p BasicProposal) GetAbstainPercentage() float64 { return p.abstain } +func (p BasicProposal) GetPendingVoterPercentage() float64 { return p.pending } + +// BasicPropDefinition implements udao.PropDefinition +type BasicPropDefinition struct { + title string + body string + created time.Time + finished time.Time +} + +func (d BasicPropDefinition) Title() string { return d.title } +func (d BasicPropDefinition) Body() string { return d.body } +func (d BasicPropDefinition) Created() time.Time { return d.created } +func (d BasicPropDefinition) Finished() time.Time { return d.finished } +func (d BasicPropDefinition) CheckConstraints() (valid bool, reasons []string) { + panic("not implemented") +} + +// BasicDAO provides a minimal implementation of the DAO interface +type BasicDAO struct { + proposals map[uint64]udao.Proposal + nextID uint64 + active *list.List + archived *list.List +} + +// NewBasicDAO creates a new BasicDAO instance +func NewBasicDAO() *BasicDAO { + return &BasicDAO{ + proposals: make(map[uint64]udao.Proposal), + nextID: 1, + active: &list.List{}, + archived: &list.List{}, + } +} + +func (d *BasicDAO) Propose(def udao.PropDefinition) (udao.Proposal, error) { + prop := BasicProposal{ + id: d.nextID, + definition: def, + state: udao.Pending, + yea: 0, + nay: 0, + abstain: 0, + pending: 100, + } + d.proposals[d.nextID] = prop + d.active.Append(prop) + d.nextID++ + return prop, nil +} + +func (d *BasicDAO) GetProposal(proposalID uint64) (udao.Proposal, error) { + if prop, ok := d.proposals[proposalID]; ok { + return prop, nil + } + return nil, nil +} + +func (d *BasicDAO) Execute(proposalID uint64) error { + if prop, ok := d.proposals[proposalID]; ok { + if prop.GetState() == udao.Passed { + newProp := prop.(BasicProposal) + newProp.state = udao.Executed + d.proposals[proposalID] = newProp + + // Move from active to archived + for i := 0; i < d.active.Len(); i++ { + if p := d.active.Get(i).(udao.Proposal); p.ID() == proposalID { + d.active.Delete(i) + d.archived.Append(newProp) + break + } + } + return nil + } + } + return nil +} + +func (d *BasicDAO) ActiveProposals() udao.PropList { + return wrap.WrapAsPropList(d.active) +} + +func (d *BasicDAO) ArchivedProposals() udao.PropList { + return wrap.WrapAsPropList(d.archived) +} + +func (d *BasicDAO) Len() int { + return len(d.proposals) +} + +// Helper methods for managing proposal state +func (d *BasicDAO) UpdateProposalState(proposalID uint64, state udao.PropState) error { + if prop, ok := d.proposals[proposalID]; ok { + newProp := prop.(BasicProposal) + newProp.state = state + d.proposals[proposalID] = newProp + return nil + } + return nil +} + +func (d *BasicDAO) UpdateVotingPercentages(proposalID uint64, yea, nay, abstain, pending float64) error { + if prop, ok := d.proposals[proposalID]; ok { + newProp := prop.(BasicProposal) + newProp.yea = yea + newProp.nay = nay + newProp.abstain = abstain + newProp.pending = pending + d.proposals[proposalID] = newProp + return nil + } + return nil +} diff --git a/examples/gno.land/p/moul/udao/basicdao/basicdao_test.gno b/examples/gno.land/p/moul/udao/basicdao/basicdao_test.gno new file mode 100644 index 00000000000..76d3c98cc8e --- /dev/null +++ b/examples/gno.land/p/moul/udao/basicdao/basicdao_test.gno @@ -0,0 +1,276 @@ +package basicdao + +import ( + "testing" + "time" + + "gno.land/p/moul/udao" +) + +func TestBasicDAO(t *testing.T) { + dao := NewBasicDAO() + if dao.Len() != 0 { + t.Errorf("expected empty DAO, got len=%d", dao.Len()) + } + + // Test proposal creation + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + + prop, err := dao.Propose(def) + if err != nil { + t.Errorf("unexpected error creating proposal: %v", err) + } + if prop.ID() != 1 { + t.Errorf("expected proposal ID=1, got %d", prop.ID()) + } + if dao.Len() != 1 { + t.Errorf("expected DAO len=1, got %d", dao.Len()) + } + + // Test proposal retrieval + retrieved, err := dao.GetProposal(1) + if err != nil { + t.Errorf("unexpected error getting proposal: %v", err) + } + if retrieved == nil { + t.Fatal("expected to retrieve proposal, got nil") + } + if retrieved.ID() != prop.ID() { + t.Errorf("expected retrieved ID=%d, got %d", prop.ID(), retrieved.ID()) + } + + // Test active proposals list + active := dao.ActiveProposals() + if active.Len() != 1 { + t.Errorf("expected 1 active proposal, got %d", active.Len()) + } + if active.Get(0).ID() != prop.ID() { + t.Errorf("expected active proposal ID=%d, got %d", prop.ID(), active.Get(0).ID()) + } + + // Test proposal execution + dao.UpdateProposalState(1, udao.Passed) + err = dao.Execute(1) + if err != nil { + t.Errorf("unexpected error executing proposal: %v", err) + } + + + executed, _ := dao.GetProposal(1) + if executed.GetState() != udao.Executed { + t.Errorf("expected state=Executed, got %s", executed.GetState()) + } + + // Test archived proposals + archived := dao.ArchivedProposals() + if archived.Len() != 1 { + t.Errorf("expected 1 archived proposal, got %d", archived.Len()) + } + if archived.Get(0).ID() != prop.ID() { + t.Errorf("expected archived proposal ID=%d, got %d", prop.ID(), archived.Get(0).ID()) + } +} + +func TestPropListWrapper(t *testing.T) { + dao := NewBasicDAO() + + // Create multiple proposals + for i := 0; i < 3; i++ { + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + dao.Propose(def) + } + + active := dao.ActiveProposals() + + // Test Len() + if active.Len() != 3 { + t.Errorf("expected len=3, got %d", active.Len()) + } + + // Test Get() + prop := active.Get(0) + if prop.ID() != 1 { + t.Errorf("expected first proposal ID=1, got %d", prop.ID()) + } + + // Test Slice() + slice := active.Slice(0, 2) + if len(slice) != 2 { + t.Errorf("expected slice len=2, got %d", len(slice)) + } + if slice[0].ID() != 1 || slice[1].ID() != 2 { + t.Errorf("unexpected slice IDs: %d, %d", slice[0].ID(), slice[1].ID()) + } +} + +func TestProposalVoting(t *testing.T) { + dao := NewBasicDAO() + + // Create a proposal + def := BasicPropDefinition{ + title: "Voting Test", + body: "Testing voting percentages", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ := dao.Propose(def) + + // Test initial voting percentages + if prop.GetYeaPercentage() != 0 { + t.Errorf("expected initial yea=0, got %f", prop.GetYeaPercentage()) + } + if prop.GetNayPercentage() != 0 { + t.Errorf("expected initial nay=0, got %f", prop.GetNayPercentage()) + } + if prop.GetPendingVoterPercentage() != 100 { + t.Errorf("expected initial pending=100, got %f", prop.GetPendingVoterPercentage()) + } + + // Update voting percentages + err := dao.UpdateVotingPercentages(prop.ID(), 60, 30, 10, 0) + if err != nil { + t.Errorf("unexpected error updating voting percentages: %v", err) + } + + // Verify updated percentages + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetYeaPercentage() != 60 { + t.Errorf("expected yea=60, got %f", updated.GetYeaPercentage()) + } + if updated.GetNayPercentage() != 30 { + t.Errorf("expected nay=30, got %f", updated.GetNayPercentage()) + } + if updated.GetAbstainPercentage() != 10 { + t.Errorf("expected abstain=10, got %f", updated.GetAbstainPercentage()) + } + if updated.GetPendingVoterPercentage() != 0 { + t.Errorf("expected pending=0, got %f", updated.GetPendingVoterPercentage()) + } +} + +func TestProposalStateTransitions(t *testing.T) { + dao := NewBasicDAO() + + // Create a proposal + def := BasicPropDefinition{ + title: "State Test", + body: "Testing state transitions", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ := dao.Propose(def) + + // Test initial state + if prop.GetState() != udao.Pending { + t.Errorf("expected initial state=Pending, got %s", prop.GetState()) + } + + // Test state transitions + states := []udao.PropState{udao.Active, udao.Passed, udao.Failed, udao.Executed, udao.Cancelled} + for _, state := range states { + err := dao.UpdateProposalState(prop.ID(), state) + if err != nil { + t.Errorf("unexpected error updating state to %s: %v", state, err) + } + + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetState() != state { + t.Errorf("expected state=%s, got %s", state, updated.GetState()) + } + } +} + +func TestInvalidProposalOperations(t *testing.T) { + dao := NewBasicDAO() + + // Test getting non-existent proposal + prop, err := dao.GetProposal(999) + if err != nil { + t.Errorf("expected nil error for non-existent proposal, got %v", err) + } + if prop != nil { + t.Error("expected nil proposal for non-existent ID") + } + + // Test executing non-existent proposal + err = dao.Execute(999) + if err != nil { + t.Errorf("expected nil error for executing non-existent proposal, got %v", err) + } + + // Test executing non-passed proposal + def := BasicPropDefinition{ + title: "Invalid Execute", + body: "Testing invalid execution", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + prop, _ = dao.Propose(def) + err = dao.Execute(prop.ID()) + if err != nil { + t.Errorf("expected nil error for executing non-passed proposal, got %v", err) + } + + // Verify state didn't change + updated, _ := dao.GetProposal(prop.ID()) + if updated.GetState() != udao.Pending { + t.Errorf("expected state to remain Pending, got %s", updated.GetState()) + } +} + +func TestPropListSlicing(t *testing.T) { + dao := NewBasicDAO() + + // Create 5 proposals + for i := 0; i < 5; i++ { + def := BasicPropDefinition{ + title: "Test Proposal", + body: "This is a test proposal", + created: time.Now(), + finished: time.Now().Add(24 * time.Hour), + } + dao.Propose(def) + } + + active := dao.ActiveProposals() + + // Test various slice operations + testCases := []struct { + start int + end int + expected int + }{ + {0, 2, 2}, + {1, 4, 3}, + {0, 5, 5}, + {2, 3, 1}, + {4, 5, 1}, + } + + for _, tc := range testCases { + slice := active.Slice(tc.start, tc.end) + if len(slice) != tc.expected { + t.Errorf("expected slice[%d:%d] len=%d, got %d", + tc.start, tc.end, tc.expected, len(slice)) + } + + // Verify IDs are sequential + for i := range slice { + expectedID := uint64(tc.start + i + 1) + if slice[i].ID() != expectedID { + t.Errorf("expected ID=%d at index %d, got %d", + expectedID, i, slice[i].ID()) + } + } + } +} diff --git a/examples/gno.land/p/moul/udao/basicdao/gno.mod b/examples/gno.land/p/moul/udao/basicdao/gno.mod new file mode 100644 index 00000000000..bb5e9251777 --- /dev/null +++ b/examples/gno.land/p/moul/udao/basicdao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/udao/basicdao diff --git a/examples/gno.land/p/moul/udao/types.gno b/examples/gno.land/p/moul/udao/types.gno deleted file mode 100644 index 9543df9789e..00000000000 --- a/examples/gno.land/p/moul/udao/types.gno +++ /dev/null @@ -1,57 +0,0 @@ -package udao - -import ( - "time" -) - -// DAO defines the interface for proposal management -type DAO interface { - // Core proposal operations - Propose(def PropDefinition) (Proposal, error) - GetProposal(proposalID uint64) (Proposal, error) - Execute(proposalID uint64) error - - // List operations - ActiveProposals() PropList - ArchivedProposals() PropList - Len() int -} - -// PropDefinition defines the content of a proposal -type PropDefinition interface { - Title() string - Body() string - Created() time.Time - Finished() time.Time - CheckConstraints() (valid bool, reasons []string) -} - -// Proposal represents a complete proposal including its ID, definition and status -type Proposal interface { - ID() uint64 - Definition() PropDefinition - GetState() PropState - GetYeaPercentage() float64 - GetNayPercentage() float64 - GetAbstainPercentage() float64 - GetPendingVoterPercentage() float64 -} - -// PropState represents the state of a proposal -type PropState string - -const ( - Pending PropState = "pending" - Active PropState = "active" - Passed PropState = "passed" - Failed PropState = "failed" - Executed PropState = "executed" - Cancelled PropState = "cancelled" -) - -// PropList defines the read-only operations available on a proposal list -type PropList interface { - Len() int - Get(index int) Proposal - Slice(startIndex, endIndex int) []Proposal -} diff --git a/examples/gno.land/p/moul/udao/udao.gno b/examples/gno.land/p/moul/udao/udao.gno index c707d9a0d53..bc2ad441a85 100644 --- a/examples/gno.land/p/moul/udao/udao.gno +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -1,140 +1,55 @@ package udao -import ( - "time" - - "gno.land/p/demo/avl/list" +import "time" + +// DAO defines the interface for proposal management +type DAO interface { + // Core proposal operations + Propose(def PropDefinition) (Proposal, error) + GetProposal(proposalID uint64) (Proposal, error) + Execute(proposalID uint64) error + + // List operations + ActiveProposals() PropList + ArchivedProposals() PropList + Len() int +} + +// PropDefinition defines the content of a proposal +type PropDefinition interface { + Title() string + Body() string + Created() time.Time + Finished() time.Time + CheckConstraints() (valid bool, reasons []string) +} + +// Proposal represents a complete proposal including its ID, definition and status +type Proposal interface { + ID() uint64 + Definition() PropDefinition + GetState() PropState + GetYeaPercentage() float64 + GetNayPercentage() float64 + GetAbstainPercentage() float64 + GetPendingVoterPercentage() float64 +} + +// PropState represents the state of a proposal +type PropState string + +const ( + Pending PropState = "pending" + Active PropState = "active" + Passed PropState = "passed" + Failed PropState = "failed" + Executed PropState = "executed" + Cancelled PropState = "cancelled" ) -// BasicProposal implements the Proposal interface -type BasicProposal struct { - id uint64 - definition PropDefinition - state PropState - yea float64 - nay float64 - abstain float64 - pending float64 -} - -func (p BasicProposal) ID() uint64 { return p.id } -func (p BasicProposal) Definition() PropDefinition { return p.definition } -func (p BasicProposal) GetState() PropState { return p.state } -func (p BasicProposal) GetYeaPercentage() float64 { return p.yea } -func (p BasicProposal) GetNayPercentage() float64 { return p.nay } -func (p BasicProposal) GetAbstainPercentage() float64 { return p.abstain } -func (p BasicProposal) GetPendingVoterPercentage() float64 { return p.pending } - -// BasicPropDefinition implements PropDefinition -type BasicPropDefinition struct { - title string - body string - created time.Time - finished time.Time -} - -func (d BasicPropDefinition) Title() string { return d.title } -func (d BasicPropDefinition) Body() string { return d.body } -func (d BasicPropDefinition) Created() time.Time { return d.created } -func (d BasicPropDefinition) Finished() time.Time { return d.finished } -func (d BasicPropDefinition) CheckConstraints() (valid bool, reasons []string) { - panic("not implemented") -} - -// BasicDAO provides a minimal implementation of the DAO interface -type BasicDAO struct { - proposals map[uint64]Proposal - nextID uint64 - active *list.List - archived *list.List -} - -// NewBasicDAO creates a new BasicDAO instance -func NewBasicDAO() *BasicDAO { - return &BasicDAO{ - proposals: make(map[uint64]Proposal), - nextID: 1, - active: &list.List{}, - archived: &list.List{}, - } -} - -func (d *BasicDAO) Propose(def PropDefinition) (Proposal, error) { - prop := BasicProposal{ - id: d.nextID, - definition: def, - state: Pending, - yea: 0, - nay: 0, - abstain: 0, - pending: 100, - } - d.proposals[d.nextID] = prop - d.active.Append(prop) - d.nextID++ - return prop, nil -} - -func (d *BasicDAO) GetProposal(proposalID uint64) (Proposal, error) { - if prop, ok := d.proposals[proposalID]; ok { - return prop, nil - } - return nil, nil -} - -func (d *BasicDAO) Execute(proposalID uint64) error { - if prop, ok := d.proposals[proposalID]; ok { - if prop.GetState() == Passed { - newProp := prop.(BasicProposal) - newProp.state = Executed - d.proposals[proposalID] = newProp - - // Move from active to archived - for i := 0; i < d.active.Len(); i++ { - if p := d.active.Get(i).(Proposal); p.ID() == proposalID { - d.active.Delete(i) - d.archived.Append(newProp) - break - } - } - return nil - } - } - return nil -} - -func (d *BasicDAO) ActiveProposals() PropList { - return WrapAsPropList(d.active) -} - -func (d *BasicDAO) ArchivedProposals() PropList { - return WrapAsPropList(d.archived) -} - -func (d *BasicDAO) Len() int { - return len(d.proposals) -} - -// Helper methods for managing proposal state -func (d *BasicDAO) UpdateProposalState(proposalID uint64, state PropState) error { - if prop, ok := d.proposals[proposalID]; ok { - newProp := prop.(BasicProposal) - newProp.state = state - d.proposals[proposalID] = newProp - return nil - } - return nil -} - -func (d *BasicDAO) UpdateVotingPercentages(proposalID uint64, yea, nay, abstain, pending float64) error { - if prop, ok := d.proposals[proposalID]; ok { - newProp := prop.(BasicProposal) - newProp.yea = yea - newProp.nay = nay - newProp.abstain = abstain - newProp.pending = pending - d.proposals[proposalID] = newProp - return nil - } - return nil +// PropList defines the read-only operations available on a proposal list +type PropList interface { + Len() int + Get(index int) Proposal + Slice(startIndex, endIndex int) []Proposal } diff --git a/examples/gno.land/p/moul/udao/udao_test.gno b/examples/gno.land/p/moul/udao/udao_test.gno index 8029bca033e..2c0644ecff6 100644 --- a/examples/gno.land/p/moul/udao/udao_test.gno +++ b/examples/gno.land/p/moul/udao/udao_test.gno @@ -1,273 +1 @@ package udao - -import ( - "testing" - "time" -) - -func TestBasicDAO(t *testing.T) { - dao := NewBasicDAO() - if dao.Len() != 0 { - t.Errorf("expected empty DAO, got len=%d", dao.Len()) - } - - // Test proposal creation - def := BasicPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - - prop, err := dao.Propose(def) - if err != nil { - t.Errorf("unexpected error creating proposal: %v", err) - } - if prop.ID() != 1 { - t.Errorf("expected proposal ID=1, got %d", prop.ID()) - } - if dao.Len() != 1 { - t.Errorf("expected DAO len=1, got %d", dao.Len()) - } - - // Test proposal retrieval - retrieved, err := dao.GetProposal(1) - if err != nil { - t.Errorf("unexpected error getting proposal: %v", err) - } - if retrieved == nil { - t.Fatal("expected to retrieve proposal, got nil") - } - if retrieved.ID() != prop.ID() { - t.Errorf("expected retrieved ID=%d, got %d", prop.ID(), retrieved.ID()) - } - - // Test active proposals list - active := dao.ActiveProposals() - if active.Len() != 1 { - t.Errorf("expected 1 active proposal, got %d", active.Len()) - } - if active.Get(0).ID() != prop.ID() { - t.Errorf("expected active proposal ID=%d, got %d", prop.ID(), active.Get(0).ID()) - } - - // Test proposal execution - dao.UpdateProposalState(1, Passed) - err = dao.Execute(1) - if err != nil { - t.Errorf("unexpected error executing proposal: %v", err) - } - - executed, _ := dao.GetProposal(1) - if executed.GetState() != Executed { - t.Errorf("expected state=Executed, got %s", executed.GetState()) - } - - // Test archived proposals - archived := dao.ArchivedProposals() - if archived.Len() != 1 { - t.Errorf("expected 1 archived proposal, got %d", archived.Len()) - } - if archived.Get(0).ID() != prop.ID() { - t.Errorf("expected archived proposal ID=%d, got %d", prop.ID(), archived.Get(0).ID()) - } -} - -func TestPropListWrapper(t *testing.T) { - dao := NewBasicDAO() - - // Create multiple proposals - for i := 0; i < 3; i++ { - def := BasicPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - dao.Propose(def) - } - - active := dao.ActiveProposals() - - // Test Len() - if active.Len() != 3 { - t.Errorf("expected len=3, got %d", active.Len()) - } - - // Test Get() - prop := active.Get(0) - if prop.ID() != 1 { - t.Errorf("expected first proposal ID=1, got %d", prop.ID()) - } - - // Test Slice() - slice := active.Slice(0, 2) - if len(slice) != 2 { - t.Errorf("expected slice len=2, got %d", len(slice)) - } - if slice[0].ID() != 1 || slice[1].ID() != 2 { - t.Errorf("unexpected slice IDs: %d, %d", slice[0].ID(), slice[1].ID()) - } -} - -func TestProposalVoting(t *testing.T) { - dao := NewBasicDAO() - - // Create a proposal - def := BasicPropDefinition{ - title: "Voting Test", - body: "Testing voting percentages", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - prop, _ := dao.Propose(def) - - // Test initial voting percentages - if prop.GetYeaPercentage() != 0 { - t.Errorf("expected initial yea=0, got %f", prop.GetYeaPercentage()) - } - if prop.GetNayPercentage() != 0 { - t.Errorf("expected initial nay=0, got %f", prop.GetNayPercentage()) - } - if prop.GetPendingVoterPercentage() != 100 { - t.Errorf("expected initial pending=100, got %f", prop.GetPendingVoterPercentage()) - } - - // Update voting percentages - err := dao.UpdateVotingPercentages(prop.ID(), 60, 30, 10, 0) - if err != nil { - t.Errorf("unexpected error updating voting percentages: %v", err) - } - - // Verify updated percentages - updated, _ := dao.GetProposal(prop.ID()) - if updated.GetYeaPercentage() != 60 { - t.Errorf("expected yea=60, got %f", updated.GetYeaPercentage()) - } - if updated.GetNayPercentage() != 30 { - t.Errorf("expected nay=30, got %f", updated.GetNayPercentage()) - } - if updated.GetAbstainPercentage() != 10 { - t.Errorf("expected abstain=10, got %f", updated.GetAbstainPercentage()) - } - if updated.GetPendingVoterPercentage() != 0 { - t.Errorf("expected pending=0, got %f", updated.GetPendingVoterPercentage()) - } -} - -func TestProposalStateTransitions(t *testing.T) { - dao := NewBasicDAO() - - // Create a proposal - def := BasicPropDefinition{ - title: "State Test", - body: "Testing state transitions", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - prop, _ := dao.Propose(def) - - // Test initial state - if prop.GetState() != Pending { - t.Errorf("expected initial state=Pending, got %s", prop.GetState()) - } - - // Test state transitions - states := []PropState{Active, Passed, Failed, Executed, Cancelled} - for _, state := range states { - err := dao.UpdateProposalState(prop.ID(), state) - if err != nil { - t.Errorf("unexpected error updating state to %s: %v", state, err) - } - - updated, _ := dao.GetProposal(prop.ID()) - if updated.GetState() != state { - t.Errorf("expected state=%s, got %s", state, updated.GetState()) - } - } -} - -func TestInvalidProposalOperations(t *testing.T) { - dao := NewBasicDAO() - - // Test getting non-existent proposal - prop, err := dao.GetProposal(999) - if err != nil { - t.Errorf("expected nil error for non-existent proposal, got %v", err) - } - if prop != nil { - t.Error("expected nil proposal for non-existent ID") - } - - // Test executing non-existent proposal - err = dao.Execute(999) - if err != nil { - t.Errorf("expected nil error for executing non-existent proposal, got %v", err) - } - - // Test executing non-passed proposal - def := BasicPropDefinition{ - title: "Invalid Execute", - body: "Testing invalid execution", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - prop, _ = dao.Propose(def) - err = dao.Execute(prop.ID()) - if err != nil { - t.Errorf("expected nil error for executing non-passed proposal, got %v", err) - } - - // Verify state didn't change - updated, _ := dao.GetProposal(prop.ID()) - if updated.GetState() != Pending { - t.Errorf("expected state to remain Pending, got %s", updated.GetState()) - } -} - -func TestPropListSlicing(t *testing.T) { - dao := NewBasicDAO() - - // Create 5 proposals - for i := 0; i < 5; i++ { - def := BasicPropDefinition{ - title: "Test Proposal", - body: "This is a test proposal", - created: time.Now(), - finished: time.Now().Add(24 * time.Hour), - } - dao.Propose(def) - } - - active := dao.ActiveProposals() - - // Test various slice operations - testCases := []struct { - start int - end int - expected int - }{ - {0, 2, 2}, - {1, 4, 3}, - {0, 5, 5}, - {2, 3, 1}, - {4, 5, 1}, - } - - for _, tc := range testCases { - slice := active.Slice(tc.start, tc.end) - if len(slice) != tc.expected { - t.Errorf("expected slice[%d:%d] len=%d, got %d", - tc.start, tc.end, tc.expected, len(slice)) - } - - // Verify IDs are sequential - for i := range slice { - expectedID := uint64(tc.start + i + 1) - if slice[i].ID() != expectedID { - t.Errorf("expected ID=%d at index %d, got %d", - expectedID, i, slice[i].ID()) - } - } - } -} diff --git a/examples/gno.land/p/moul/udao/wrappers.gno b/examples/gno.land/p/moul/udao/wrap/avl_list.gno similarity index 54% rename from examples/gno.land/p/moul/udao/wrappers.gno rename to examples/gno.land/p/moul/udao/wrap/avl_list.gno index ea06e439bd0..6ede8c13b34 100644 --- a/examples/gno.land/p/moul/udao/wrappers.gno +++ b/examples/gno.land/p/moul/udao/wrap/avl_list.gno @@ -1,7 +1,8 @@ -package udao +package wrap import ( "gno.land/p/demo/avl/list" + "gno.land/p/moul/udao" ) // PropListWrapper wraps an IReadOnlyList to implement PropList @@ -10,7 +11,7 @@ type PropListWrapper struct { } // WrapAsPropList converts an IReadOnlyList to a PropList -func WrapAsPropList(list list.IList) PropList { +func WrapAsPropList(list list.IList) udao.PropList { return &PropListWrapper{list: list} } @@ -18,15 +19,15 @@ func (pl *PropListWrapper) Len() int { return pl.list.Len() } -func (pl *PropListWrapper) Get(index int) Proposal { - return pl.list.Get(index).(Proposal) +func (pl *PropListWrapper) Get(index int) udao.Proposal { + return pl.list.Get(index).(udao.Proposal) } -func (pl *PropListWrapper) Slice(startIndex, endIndex int) []Proposal { +func (pl *PropListWrapper) Slice(startIndex, endIndex int) []udao.Proposal { slice := pl.list.Slice(startIndex, endIndex) - result := make([]Proposal, len(slice)) + result := make([]udao.Proposal, len(slice)) for i, v := range slice { - result[i] = v.(Proposal) + result[i] = v.(udao.Proposal) } return result } diff --git a/examples/gno.land/p/moul/udao/wrap/avl_list_test.gno b/examples/gno.land/p/moul/udao/wrap/avl_list_test.gno new file mode 100644 index 00000000000..a1080fa3223 --- /dev/null +++ b/examples/gno.land/p/moul/udao/wrap/avl_list_test.gno @@ -0,0 +1 @@ +package wrap diff --git a/examples/gno.land/p/moul/udao/wrap/gno.mod b/examples/gno.land/p/moul/udao/wrap/gno.mod new file mode 100644 index 00000000000..d2c0c5c6700 --- /dev/null +++ b/examples/gno.land/p/moul/udao/wrap/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/udao/wrap