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/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 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..bc2ad441a85 --- /dev/null +++ b/examples/gno.land/p/moul/udao/udao.gno @@ -0,0 +1,55 @@ +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_test.gno b/examples/gno.land/p/moul/udao/udao_test.gno new file mode 100644 index 00000000000..2c0644ecff6 --- /dev/null +++ b/examples/gno.land/p/moul/udao/udao_test.gno @@ -0,0 +1 @@ +package udao diff --git a/examples/gno.land/p/moul/udao/wrap/avl_list.gno b/examples/gno.land/p/moul/udao/wrap/avl_list.gno new file mode 100644 index 00000000000..6ede8c13b34 --- /dev/null +++ b/examples/gno.land/p/moul/udao/wrap/avl_list.gno @@ -0,0 +1,33 @@ +package wrap + +import ( + "gno.land/p/demo/avl/list" + "gno.land/p/moul/udao" +) + +// 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) udao.PropList { + return &PropListWrapper{list: list} +} + +func (pl *PropListWrapper) Len() int { + return pl.list.Len() +} + +func (pl *PropListWrapper) Get(index int) udao.Proposal { + return pl.list.Get(index).(udao.Proposal) +} + +func (pl *PropListWrapper) Slice(startIndex, endIndex int) []udao.Proposal { + slice := pl.list.Slice(startIndex, endIndex) + result := make([]udao.Proposal, len(slice)) + for i, v := range slice { + 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