diff --git a/gno/p/basedao/basedao.gno b/gno/p/basedao/basedao.gno index 42e139f12b..631dfab3d5 100644 --- a/gno/p/basedao/basedao.gno +++ b/gno/p/basedao/basedao.gno @@ -85,6 +85,8 @@ func New(conf *Config) *DAO { NewRemoveMemberHandler(dao), NewAssignRoleHandler(dao), NewUnassignRoleHandler(dao), + daokit.NewSetResourceHandler(dao.Core), + daokit.NewRemoveResourceHandler(dao.Core), } { dao.Core.SetResource(&daokit.Resource{Handler: m, Condition: conf.InitialCondition}) } diff --git a/gno/p/basedao/basedao_test.gno b/gno/p/basedao/basedao_test.gno index 98d3dcc544..7de13d3154 100644 --- a/gno/p/basedao/basedao_test.gno +++ b/gno/p/basedao/basedao_test.gno @@ -57,7 +57,7 @@ func TestNewDAO(t *testing.T) { } } - urequire.Equal(t, 5, dao.Core.ResourcesCount(), "expected 5 resources") + urequire.Equal(t, 7, dao.Core.ResourcesCount(), "expected 7 resources") urequire.Equal(t, dao.Realm.PkgPath(), daoRealm.PkgPath()) // XXX: check realm and profile @@ -725,3 +725,125 @@ func newTestingDAO(t *testing.T, threshold float64, members []Member) *testingDA unknownProposalRequest: unknownProposalRequest, } } + +func TestAddRemoveResource(t *testing.T) { + resourceType := "newResource" + members := []Member{ + { + alice.String(), + []string{"admin"}, + }, + { + bob.String(), + []string{"admin"}, + }, + } + tdao := newTestingDAO(t, 0.2, members) + + if !tdao.dao.Members.HasRole(bob.String(), "admin") { + t.Errorf("Expected member %s to have role 'admin'", bob.String()) + } + + mockHandler := daokit.NewMessageHandler(resourceType, func(_ interface{}) {}) + + addResourceProposal := daokit.ProposalRequest{ + Title: "My Proposal", + Description: "My Proposal Description", + Message: daokit.NewSetResourceMsg(&daokit.Resource{ + Handler: mockHandler, + Condition: daocond.MembersThreshold(0.2, tdao.dao.Members.IsMember, tdao.dao.Members.MembersCount), + }), + } + urequire.Equal(t, 8, tdao.dao.Core.ResourcesCount(), "expected 8 resources") + std.TestSetOrigCaller(alice) + tdao.dao.InstantExecute(addResourceProposal) + urequire.Equal(t, 9, tdao.dao.Core.ResourcesCount(), "expected 9 resources") + + removeResourceProposal := daokit.ProposalRequest{ + Title: "My Proposal", + Description: "My Proposal Description", + Message: daokit.NewRemoveResourceMsg(&daokit.Resource{ + Handler: mockHandler, + Condition: daocond.MembersThreshold(0.2, tdao.dao.Members.IsMember, tdao.dao.Members.MembersCount), + }), + } + + tdao.dao.InstantExecute(removeResourceProposal) + + urequire.Equal(t, 8, tdao.dao.Core.ResourcesCount(), "expected 8 resources") +} + +func TestAddRemoveResourceInvalidateProposals(t *testing.T) { + resourceType := "newResource" + members := []Member{ + { + alice.String(), + []string{"admin"}, + }, + { + bob.String(), + []string{"admin"}, + }, + } + tdao := newTestingDAO(t, 0.2, members) + + if !tdao.dao.Members.HasRole(bob.String(), "admin") { + t.Errorf("Expected member %s to have role 'admin'", bob.String()) + } + + mockHandler := daokit.NewMessageHandler(resourceType, func(_ interface{}) {}) + + addResourceProposal := daokit.ProposalRequest{ + Title: "My Proposal", + Description: "My Proposal Description", + Message: daokit.NewSetResourceMsg(&daokit.Resource{ + Handler: mockHandler, + Condition: daocond.MembersThreshold(0.2, tdao.dao.Members.IsMember, tdao.dao.Members.MembersCount), + }), + } + std.TestSetOrigCaller(alice) + tdao.dao.InstantExecute(addResourceProposal) + + proposalAddMember := daokit.ProposalRequest{ + Message: daokit.NewMessage(MsgAddMemberKind, nil), + } + + newResourceProposal := daokit.ProposalRequest{ + Message: daokit.NewMessage(resourceType, nil), + } + + tdao.dao.Propose(newResourceProposal) + tdao.dao.Propose(proposalAddMember) + + // Check proposal is open + tdao.dao.Core.ProposalModule.Proposals.Iterate("", "", func(k string, v interface{}) bool { + p := v.(*daokit.Proposal) + if p.Message.Type() == resourceType { + urequire.Equal(t, int(daokit.ProposalStatusOpen), int(p.Status), "expected to be open") + } else if p.Message.Type() == MsgAddMemberKind { + urequire.Equal(t, int(daokit.ProposalStatusOpen), int(p.Status), "expected to be open") + } + return false + }) + + removeResourceProposal := daokit.ProposalRequest{ + Title: "removeResource", + Description: "My Proposal Description", + Message: daokit.NewRemoveResourceMsg(&daokit.Resource{ + Handler: mockHandler, + Condition: daocond.MembersThreshold(0.2, tdao.dao.Members.IsMember, tdao.dao.Members.MembersCount), + }), + } + tdao.dao.InstantExecute(removeResourceProposal) + + // proposal should be invalidated + tdao.dao.Core.ProposalModule.Proposals.Iterate("", "", func(k string, v interface{}) bool { + p := v.(*daokit.Proposal) + if p.Message.Type() == resourceType { + urequire.Equal(t, int(daokit.ProposalStatusInvalidated), int(p.Status), "expected to be invalidated") + } else if p.Message.Type() == MsgAddMemberKind { + urequire.Equal(t, int(daokit.ProposalStatusOpen), int(p.Status), "expected to stay open") + } + return false + }) +} diff --git a/gno/p/daokit/messages.gno b/gno/p/daokit/messages.gno index 6b5bc670b9..ecfdd49225 100644 --- a/gno/p/daokit/messages.gno +++ b/gno/p/daokit/messages.gno @@ -64,3 +64,41 @@ func (g *genericMessageHandler) Instantiate() ExecutableMessage { func (g *genericMessageHandler) Type() string { return g.kind } + +const MsgSetResourceKind = "gno.land/p/teritori/daokit.SetResource" + +func NewSetResourceHandler(d *Core) MessageHandler { + return NewMessageHandler(MsgSetResourceKind, func(ipayload interface{}) { + payload, ok := ipayload.(*Resource) + if !ok { + panic(errors.New("invalid payload type")) + } + + d.SetResource(payload) + d.ProposalModule.InvalidateWithResource(payload.Handler.Type()) + }) +} + +func NewSetResourceMsg(payload *Resource) ExecutableMessage { + return NewMessage(MsgSetResourceKind, payload) +} + +const MsgRemoveResourceKind = "gno.land/p/teritori/daokit.RemoveResource" + +func NewRemoveResourceHandler(d *Core) MessageHandler { + return NewMessageHandler(MsgRemoveResourceKind, func(ipayload interface{}) { + payload, ok := ipayload.(*Resource) + if !ok { + panic(errors.New("invalid payload type")) + } + if d.Resources.getResource(payload.Handler.Type()) == nil { + panic("ressource " + payload.Handler.Type() + " does not exists") + } + d.Resources.removeResource(payload) + d.ProposalModule.InvalidateWithResource(payload.Handler.Type()) + }) +} + +func NewRemoveResourceMsg(payload *Resource) ExecutableMessage { + return NewMessage(MsgRemoveResourceKind, payload) +} diff --git a/gno/p/daokit/proposals.gno b/gno/p/daokit/proposals.gno index 1c42ed2305..3216caef9a 100644 --- a/gno/p/daokit/proposals.gno +++ b/gno/p/daokit/proposals.gno @@ -17,6 +17,7 @@ const ( ProposalStatusOpen ProposalStatus = iota ProposalStatusPassed ProposalStatusExecuted + ProposalStatusInvalidated ) func (s ProposalStatus) String() string { @@ -27,6 +28,8 @@ func (s ProposalStatus) String() string { return "Passed" case ProposalStatusExecuted: return "Executed" + case ProposalStatusInvalidated: + return "Invalidated" default: return "Unknown" } @@ -91,6 +94,23 @@ func (p *ProposalModule) GetProposal(id uint64) *Proposal { return proposal } +func (p *ProposalModule) InvalidateWithResource(resourceType string) { + toInvalidate := map[string]*Proposal{} + + p.Proposals.Iterate("", "", func(k string, v interface{}) bool { + proposal := v.(*Proposal) + if proposal.Status == ProposalStatusOpen && proposal.Message.Type() == resourceType { + proposal.Status = ProposalStatusInvalidated + toInvalidate[k] = proposal + } + return false + }) + + for k, v := range toInvalidate { + p.Proposals.Set(k, v) + } +} + func (p *Proposal) UpdateStatus() { conditionsAreMet := p.ConditionState.Eval(p.Votes) if p.Status == ProposalStatusOpen && conditionsAreMet { diff --git a/gno/p/daokit/resources.gno b/gno/p/daokit/resources.gno index d517b40e8b..1d5c470968 100644 --- a/gno/p/daokit/resources.gno +++ b/gno/p/daokit/resources.gno @@ -24,6 +24,10 @@ func (r *resourcesStore) setResource(resource *Resource) { r.Resources.Set(resource.Handler.Type(), resource) } +func (r *resourcesStore) removeResource(resource *Resource) { + r.Resources.Remove(resource.Handler.Type()) +} + func (r *resourcesStore) getResource(name string) *Resource { value, ok := r.Resources.Get(name) if !ok {