From 918b2c0a01c4cfabdc1260cd7caf4e0d0ea24458 Mon Sep 17 00:00:00 2001 From: n0izn0iz Date: Tue, 7 Jan 2025 16:24:17 +0100 Subject: [PATCH 1/2] feat(gno): dao conditions (#1479) Signed-off-by: Norman --- gno/p/daocond/cond_and.gno | 66 ++++ gno/p/daocond/cond_members_threshold.gno | 99 ++++++ .../cond_members_threshold_few_votes.gno | 91 ++++++ gno/p/daocond/cond_or.gno | 66 ++++ gno/p/daocond/cond_role_count.gno | 115 +++++++ gno/p/daocond/cond_role_count_few_votes.gno | 92 ++++++ gno/p/daocond/daocond.gno | 65 ++++ gno/p/daocond/daocond_test.gno | 293 ++++++++++++++++++ gno/p/daocond/gno.mod | 1 + 9 files changed, 888 insertions(+) create mode 100644 gno/p/daocond/cond_and.gno create mode 100644 gno/p/daocond/cond_members_threshold.gno create mode 100644 gno/p/daocond/cond_members_threshold_few_votes.gno create mode 100644 gno/p/daocond/cond_or.gno create mode 100644 gno/p/daocond/cond_role_count.gno create mode 100644 gno/p/daocond/cond_role_count_few_votes.gno create mode 100644 gno/p/daocond/daocond.gno create mode 100644 gno/p/daocond/daocond_test.gno create mode 100644 gno/p/daocond/gno.mod diff --git a/gno/p/daocond/cond_and.gno b/gno/p/daocond/cond_and.gno new file mode 100644 index 0000000000..eea77ea802 --- /dev/null +++ b/gno/p/daocond/cond_and.gno @@ -0,0 +1,66 @@ +package daocond + +import ( + "gno.land/p/demo/json" +) + +func And(left Condition, right Condition) Condition { + if left == nil || right == nil { + panic("left or right is nil") + } + return &andCond{left: left, right: right} +} + +type andCond struct { + // XXX: use a slice instead of only two children? + left Condition + right Condition +} + +// NewState implements Condition. +func (a *andCond) NewState() State { + return &andState{left: a.left.NewState(), right: a.right.NewState()} +} + +// Render implements Condition. +func (a *andCond) Render() string { + return "[" + a.left.Render() + " AND " + a.right.Render() + "]" +} + +// RenderJSON implements Condition. +func (a *andCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "and"), + "left": a.left.RenderJSON(), + "right": a.right.RenderJSON(), + }) +} + +var _ Condition = (*andCond)(nil) + +type andState struct { + left State + right State +} + +// RenderJSON implements State. +func (a *andState) RenderJSON(votes map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "and"), + "left": a.left.RenderJSON(votes), + "right": a.right.RenderJSON(votes), + }) +} + +// Eval implements State. +func (a *andState) Eval(votes map[string]Vote) bool { + return a.left.Eval(votes) && a.right.Eval(votes) +} + +// HandleEvent implements State. +func (a *andState) HandleEvent(evt Event, votes map[string]Vote) { + a.left.HandleEvent(evt, votes) + a.right.HandleEvent(evt, votes) +} + +var _ State = (*andState)(nil) diff --git a/gno/p/daocond/cond_members_threshold.gno b/gno/p/daocond/cond_members_threshold.gno new file mode 100644 index 0000000000..aab1dbc0ad --- /dev/null +++ b/gno/p/daocond/cond_members_threshold.gno @@ -0,0 +1,99 @@ +package daocond + +import ( + "errors" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +func MembersThreshold(threshold float64, isMemberFn func(memberId string) bool, membersCountFn func() uint64) Condition { + if threshold <= 0 || threshold > 1 { + panic(errors.New("invalid threshold")) + } + if isMemberFn == nil { + panic(errors.New("nil isMemberFn")) + } + if membersCountFn == nil { + panic(errors.New("nil membersCountFn")) + } + return &membersThresholdCond{ + threshold: threshold, + isMemberFn: isMemberFn, + membersCountFn: membersCountFn, + } +} + +type membersThresholdCond struct { + isMemberFn func(memberId string) bool + membersCountFn func() uint64 + threshold float64 +} + +// NewState implements Condition. +func (m *membersThresholdCond) NewState() State { + return &membersThresholdState{ + cond: m, + } +} + +// Render implements Condition. +func (m *membersThresholdCond) Render() string { + return ufmt.Sprintf("%g%% of members", m.threshold*100) +} + +// RenderJSON implements Condition. +func (m *membersThresholdCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "members-threshold"), + "threshold": json.NumberNode("", m.threshold), + }) +} + +var _ Condition = (*membersThresholdCond)(nil) + +type membersThresholdState struct { + cond *membersThresholdCond + totalYes uint64 +} + +// Eval implements State. +func (m *membersThresholdState) Eval(_ map[string]Vote) bool { + return float64(m.totalYes)/float64(m.cond.membersCountFn()) >= m.cond.threshold +} + +// HandleEvent implements State. +func (m *membersThresholdState) HandleEvent(evt Event, votes map[string]Vote) { + switch evt := evt.(type) { + case *EventVote: + if !m.cond.isMemberFn(evt.VoterID) { + return + } + previousVote := votes[evt.VoterID] + if previousVote == VoteYes && evt.Vote != VoteYes { + m.totalYes -= 1 + } else if previousVote != VoteYes && evt.Vote == VoteYes { + m.totalYes += 1 + } + + case *EventMemberAdded: + if votes[evt.MemberID] == VoteYes { + m.totalYes += 1 + } + + case *EventMemberRemoved: + if votes[evt.MemberID] == VoteYes { + m.totalYes -= 1 + } + } +} + +// RenderJSON implements State. +func (m *membersThresholdState) RenderJSON(_ map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "members-threshold"), + "totalYes": json.NumberNode("", float64(m.totalYes)), + }) +} + +var _ State = (*membersThresholdState)(nil) diff --git a/gno/p/daocond/cond_members_threshold_few_votes.gno b/gno/p/daocond/cond_members_threshold_few_votes.gno new file mode 100644 index 0000000000..2e9badf644 --- /dev/null +++ b/gno/p/daocond/cond_members_threshold_few_votes.gno @@ -0,0 +1,91 @@ +package daocond + +import ( + "errors" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +func MembersThresholdFewVotes(threshold float64, isMemberFn func(memberId string) bool, membersCountFn func() uint64) Condition { + if threshold <= 0 || threshold > 1 { + panic(errors.New("invalid threshold")) + } + if isMemberFn == nil { + panic(errors.New("nil isMemberFn")) + } + if membersCountFn == nil { + panic(errors.New("nil membersCountFn")) + } + return &membersThresholdFewVotesCond{ + threshold: threshold, + isMemberFn: isMemberFn, + membersCountFn: membersCountFn, + } +} + +type membersThresholdFewVotesCond struct { + isMemberFn func(memberId string) bool + membersCountFn func() uint64 + threshold float64 +} + +// NewState implements Condition. +func (m *membersThresholdFewVotesCond) NewState() State { + return &membersThresholdFewVotesState{ + cond: m, + } +} + +// Render implements Condition. +func (m *membersThresholdFewVotesCond) Render() string { + return ufmt.Sprintf("%g%% of members", m.threshold*100) +} + +// RenderJSON implements Condition. +func (m *membersThresholdFewVotesCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "members-threshold"), + "threshold": json.NumberNode("", m.threshold), + }) +} + +var _ Condition = (*membersThresholdFewVotesCond)(nil) + +type membersThresholdFewVotesState struct { + cond *membersThresholdFewVotesCond +} + +func (m *membersThresholdFewVotesState) totalYes(votes map[string]Vote) uint64 { + totalYes := uint64(0) + for userId, vote := range votes { + if vote != VoteYes { + continue + } + if !m.cond.isMemberFn(userId) { + continue + } + totalYes += 1 + } + return totalYes +} + +// Eval implements State. +func (m *membersThresholdFewVotesState) Eval(votes map[string]Vote) bool { + return float64(m.totalYes(votes))/float64(m.cond.membersCountFn()) >= m.cond.threshold +} + +// HandleEvent implements State. +func (m *membersThresholdFewVotesState) HandleEvent(_ Event, _ map[string]Vote) { + panic(errors.New("not implemented")) +} + +// RenderJSON implements State. +func (m *membersThresholdFewVotesState) RenderJSON(votes map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "members-threshold"), + "totalYes": json.NumberNode("", float64(m.totalYes(votes))), + }) +} + +var _ State = (*membersThresholdFewVotesState)(nil) diff --git a/gno/p/daocond/cond_or.gno b/gno/p/daocond/cond_or.gno new file mode 100644 index 0000000000..de65d7d46f --- /dev/null +++ b/gno/p/daocond/cond_or.gno @@ -0,0 +1,66 @@ +package daocond + +import ( + "gno.land/p/demo/json" +) + +func Or(left Condition, right Condition) Condition { + if left == nil || right == nil { + panic("left or right is nil") + } + return &orCond{left: left, right: right} +} + +type orCond struct { + // XXX: use a slice instead of only two children? + left Condition + right Condition +} + +// NewState implements Condition. +func (a *orCond) NewState() State { + return &orState{left: a.left.NewState(), right: a.right.NewState()} +} + +// Render implements Condition. +func (a *orCond) Render() string { + return "[" + a.left.Render() + " OR " + a.right.Render() + "]" +} + +// RenderJSON implements Condition. +func (a *orCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "or"), + "left": a.left.RenderJSON(), + "right": a.right.RenderJSON(), + }) +} + +var _ Condition = (*andCond)(nil) + +type orState struct { + left State + right State +} + +// Eval implements State. +func (a *orState) Eval(votes map[string]Vote) bool { + return a.left.Eval(votes) || a.right.Eval(votes) +} + +// HandleEvent implements State. +func (a *orState) HandleEvent(evt Event, votes map[string]Vote) { + a.left.HandleEvent(evt, votes) + a.right.HandleEvent(evt, votes) +} + +// RenderJSON implements State. +func (a *orState) RenderJSON(votes map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "and"), + "left": a.left.RenderJSON(votes), + "right": a.right.RenderJSON(votes), + }) +} + +var _ State = (*orState)(nil) diff --git a/gno/p/daocond/cond_role_count.gno b/gno/p/daocond/cond_role_count.gno new file mode 100644 index 0000000000..ee38035c6f --- /dev/null +++ b/gno/p/daocond/cond_role_count.gno @@ -0,0 +1,115 @@ +package daocond + +import ( + "errors" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +func RoleCount(count uint64, role string, hasRoleFn func(memberId string, role string) bool) Condition { + if count == 0 { + panic(errors.New("count must be greater than 0")) + } + if role == "" { + panic(errors.New("role must not be empty")) + } + if hasRoleFn == nil { + panic(errors.New("nil hasRoleFn")) + } + return &roleCountCond{ + count: count, + hasRoleFn: hasRoleFn, + role: role, + } +} + +type roleCountCond struct { + hasRoleFn func(memberId string, role string) bool + count uint64 + role string +} + +// NewState implements Condition. +func (m *roleCountCond) NewState() State { + return &roleCountState{ + cond: m, + } +} + +// Render implements Condition. +func (m *roleCountCond) Render() string { + return ufmt.Sprintf("%d %s", m.count, m.role) +} + +// RenderJSON implements Condition. +func (m *roleCountCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-count"), + "role": json.StringNode("", m.role), + "count": json.NumberNode("", float64(m.count)), + }) +} + +var _ Condition = (*roleCountCond)(nil) + +type roleCountState struct { + cond *roleCountCond + totalYes uint64 +} + +// Eval implements State. +func (m *roleCountState) Eval(_ map[string]Vote) bool { + return m.totalYes >= m.cond.count +} + +// HandleEvent implements State. +func (m *roleCountState) HandleEvent(evt Event, votes map[string]Vote) { + switch evt := evt.(type) { + case *EventVote: + if !m.cond.hasRoleFn(evt.VoterID, m.cond.role) { + return + } + previousVote := votes[evt.VoterID] + if previousVote == VoteYes && evt.Vote != VoteYes { + m.totalYes -= 1 + } + if previousVote != VoteYes && evt.Vote == VoteYes { + m.totalYes += 1 + } + + case *EventRoleAssigned: + if evt.Role != m.cond.role { + return + } + vote := votes[evt.UserID] + if vote == VoteYes { + m.totalYes += 1 + } + + case *EventRoleUnassigned: + if evt.Role != m.cond.role { + return + } + vote := votes[evt.UserID] + if vote == VoteYes { + m.totalYes -= 1 + } + + case *EventRoleRemoved: + if evt.Role != m.cond.role { + return + } + m.totalYes = 0 + } +} + +// RenderJSON implements State. +func (m *roleCountState) RenderJSON(_ map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-count"), + "totalYes": json.NumberNode("", float64(m.totalYes)), + }) +} + +var _ State = (*roleCountState)(nil) diff --git a/gno/p/daocond/cond_role_count_few_votes.gno b/gno/p/daocond/cond_role_count_few_votes.gno new file mode 100644 index 0000000000..09a4b0d477 --- /dev/null +++ b/gno/p/daocond/cond_role_count_few_votes.gno @@ -0,0 +1,92 @@ +package daocond + +import ( + "errors" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +func RoleCountFewVotes(count uint64, role string, hasRoleFn func(memberId string, role string) bool) Condition { + if count == 0 { + panic(errors.New("count must be greater than 0")) + } + if role == "" { + panic(errors.New("role must not be empty")) + } + if hasRoleFn == nil { + panic(errors.New("nil hasRoleFn")) + } + return &roleCountFewVotesCond{ + count: count, + hasRoleFn: hasRoleFn, + role: role, + } +} + +type roleCountFewVotesCond struct { + hasRoleFn func(memberId string, role string) bool + count uint64 + role string +} + +// NewState implements Condition. +func (m *roleCountFewVotesCond) NewState() State { + return &roleCountFewVotesState{ + cond: m, + } +} + +// Render implements Condition. +func (m *roleCountFewVotesCond) Render() string { + return ufmt.Sprintf("%d %s", m.count, m.role) +} + +// RenderJSON implements Condition. +func (m *roleCountFewVotesCond) RenderJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-count-few-votes"), + "role": json.StringNode("", m.role), + "count": json.NumberNode("", float64(m.count)), + }) +} + +var _ Condition = (*roleCountFewVotesCond)(nil) + +type roleCountFewVotesState struct { + cond *roleCountFewVotesCond +} + +func (m *roleCountFewVotesState) totalYes(votes map[string]Vote) uint64 { + totalYes := uint64(0) + for userId, vote := range votes { + if vote != VoteYes { + continue + } + if !m.cond.hasRoleFn(userId, m.cond.role) { + continue + } + totalYes += 1 + } + return totalYes +} + +// Eval implements State. +func (m *roleCountFewVotesState) Eval(votes map[string]Vote) bool { + return m.totalYes(votes) >= m.cond.count +} + +// HandleEvent implements State. +func (m *roleCountFewVotesState) HandleEvent(_ Event, _ map[string]Vote) { + panic(errors.New("not implemented")) +} + +// RenderJSON implements State. +func (m *roleCountFewVotesState) RenderJSON(votes map[string]Vote) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", "role-count-few-votes"), + "totalYes": json.NumberNode("", float64(m.totalYes(votes))), + }) +} + +var _ State = (*roleCountFewVotesState)(nil) diff --git a/gno/p/daocond/daocond.gno b/gno/p/daocond/daocond.gno new file mode 100644 index 0000000000..927926645c --- /dev/null +++ b/gno/p/daocond/daocond.gno @@ -0,0 +1,65 @@ +package daocond + +import ( + "gno.land/p/demo/json" +) + +// This model should work pretty good for small and dynamic daos. +// For daos with a lot of members and active proposals we might need to get rid of change events. + +// base interfaces + +type Condition interface { + NewState() State + + Render() string + RenderJSON() *json.Node +} + +type State interface { + Eval(votes map[string]Vote) bool + HandleEvent(event Event, votes map[string]Vote) + + RenderJSON(votes map[string]Vote) *json.Node +} + +type Vote string + +const ( + VoteYes = "yes" + VoteNo = "no" + VoteAbstain = "abstain" +) + +type Event interface { +} + +// well known events + +type EventVote struct { + VoterID string + Vote Vote +} + +type EventMemberAdded struct { + MemberID string +} + +type EventMemberRemoved struct { + MemberID string +} + +type EventRoleAssigned struct { + UserID string + Role string +} + +type EventRoleUnassigned struct { + UserID string + Role string +} + +type EventRoleRemoved struct { + UserID string + Role string +} diff --git a/gno/p/daocond/daocond_test.gno b/gno/p/daocond/daocond_test.gno new file mode 100644 index 0000000000..0f1c51cff2 --- /dev/null +++ b/gno/p/daocond/daocond_test.gno @@ -0,0 +1,293 @@ +package daocond_test + +import ( + "errors" + "testing" + + "gno.land/p/demo/urequire" + "gno.land/p/teritori/daocond" +) + +func TestCondition(t *testing.T) { + dao := newMockDAO(nil) + + // leaf conditions + membersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount) + publicRelationships := daocond.RoleCount(1, "public-relationships", dao.hasRole) + financeOfficer := daocond.RoleCount(1, "finance-officer", dao.hasRole) + + urequire.Equal(t, "60% of members", membersMajority.Render()) + urequire.Equal(t, "1 public-relationships", publicRelationships.Render()) + urequire.Equal(t, "1 finance-officer", financeOfficer.Render()) + + // ressource expressions + ressources := map[string]daocond.Condition{ + "social.post": daocond.And(publicRelationships, membersMajority), + "finance.invest": daocond.Or(financeOfficer, membersMajority), + } + + urequire.Equal(t, "[1 public-relationships AND 60% of members]", ressources["social.post"].Render()) + urequire.Equal(t, "[1 finance-officer OR 60% of members]", ressources["finance.invest"].Render()) + +} + +func TestState(t *testing.T) { + setups := []struct { + name string + setup func(dao *mockDAO) + }{ + {name: "basic", setup: func(dao *mockDAO) { + membersMajority := daocond.MembersThreshold(0.6, dao.isMember, dao.membersCount) + publicRelationships := daocond.RoleCount(1, "public-relationships", dao.hasRole) + financeOfficer := daocond.RoleCount(1, "finance-officer", dao.hasRole) + dao.resources = map[string]daocond.Condition{ + "social.post": daocond.And(publicRelationships, membersMajority), + "finance.invest": daocond.Or(financeOfficer, membersMajority), + } + }}, + {name: "few-votes", setup: func(dao *mockDAO) { + dao.noEvents = true + membersMajority := daocond.MembersThresholdFewVotes(0.6, dao.isMember, dao.membersCount) + publicRelationships := daocond.RoleCountFewVotes(1, "public-relationships", dao.hasRole) + financeOfficer := daocond.RoleCountFewVotes(1, "finance-officer", dao.hasRole) + dao.resources = map[string]daocond.Condition{ + "social.post": daocond.And(publicRelationships, membersMajority), + "finance.invest": daocond.Or(financeOfficer, membersMajority), + } + }}, + } + + cases := []struct { + name string + resource string + phases []testPhase + }{ + { + name: "post with public-relationships", + resource: "social.post", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "yes", + "bob": "yes", + "eve": "no", + }, + result: true, + }}, + }, + { + name: "post without public-relationships", + resource: "social.post", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "yes", + "bob": "no", + "eve": "yes", + }, + result: false, + }}, + }, + { + name: "post after public-relationships changes", + resource: "social.post", + phases: []testPhase{ + { + votes: map[string]daocond.Vote{ + "alice": "yes", + "bob": "yes", + "eve": "no", + }, + result: true, + }, + { + changes: func(dao *mockDAO) { + dao.unassignRole("bob", "public-relationships") + }, + result: false, + }, + { + changes: func(dao *mockDAO) { + dao.assignRole("alice", "public-relationships") + }, + result: true, + }, + }, + }, + { + name: "post public-relationships alone", + resource: "social.post", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "no", + "bob": "yes", + "eve": "no", + }, + result: false, + }}, + }, + { + name: "invest with finance officer", + resource: "finance.invest", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "yes", + "bob": "no", + "eve": "no", + }, + result: true, + }}, + }, + { + name: "invest without finance officer", + resource: "finance.invest", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "no", + "bob": "yes", + "eve": "yes", + }, + result: true, + }}, + }, + { + name: "invest alone", + resource: "finance.invest", + phases: []testPhase{{ + votes: map[string]daocond.Vote{ + "alice": "no", + "bob": "no", + "eve": "yes", + }, + result: false, + }}, + }, + } + + for _, tc := range cases { + for _, s := range setups { + t.Run(tc.name+" "+s.name, func(t *testing.T) { + var handleEvt func(evt daocond.Event) + dao := newMockDAO(func(evt daocond.Event) { handleEvt(evt) }) + s.setup(dao) + + resource, ok := dao.resources[tc.resource] + urequire.True(t, ok) + + state := resource.NewState() + ballots := map[string]daocond.Vote{} + if !dao.noEvents { + handleEvt = func(evt daocond.Event) { state.HandleEvent(evt, ballots) } + } + + for _, phase := range tc.phases { + if phase.changes != nil { + phase.changes(dao) + } + if phase.votes != nil { + for memberId, vote := range phase.votes { + if !dao.noEvents { + handleEvt(&daocond.EventVote{VoterID: memberId, Vote: vote}) + } + ballots[memberId] = vote + } + } + result := state.Eval(ballots) + if phase.result != result { + println("State:", state.RenderJSON(ballots)) + } + urequire.Equal(t, phase.result, result) + } + }) + } + } +} + +type testPhase struct { + changes func(dao *mockDAO) + votes map[string]daocond.Vote + result bool +} + +type mockDAO struct { + emitter func(evt daocond.Event) + members map[string][]string + noEvents bool + resources map[string]daocond.Condition +} + +func newMockDAO(emitter func(evt daocond.Event)) *mockDAO { + return &mockDAO{ + emitter: emitter, + members: map[string][]string{ + "alice": []string{"finance-officer"}, + "bob": []string{"public-relationships"}, + "eve": []string{}, + }, + resources: make(map[string]daocond.Condition), + } +} + +func (m *mockDAO) assignRole(userId string, role string) { + roles, ok := m.members[userId] + if !ok { + panic(errors.New("unknown member")) + } + m.members[userId], ok = strsadd(roles, role) + if ok && !m.noEvents { + m.emitter(&daocond.EventRoleAssigned{UserID: userId, Role: role}) + } +} + +func (m *mockDAO) unassignRole(userId string, role string) { + roles, ok := m.members[userId] + if !ok { + panic(errors.New("unknown member")) + } + m.members[userId], ok = strsrm(roles, role) + if ok && !m.noEvents { + m.emitter(&daocond.EventRoleUnassigned{UserID: userId, Role: role}) + } +} + +func (m *mockDAO) isMember(memberId string) bool { + _, ok := m.members[memberId] + return ok +} + +func (m *mockDAO) membersCount() uint64 { + return uint64(len(m.members)) +} + +func (m *mockDAO) hasRole(memberId string, role string) bool { + roles, ok := m.members[memberId] + if !ok { + return false + } + for _, memberRole := range roles { + if memberRole == role { + return true + } + } + return false +} + +func strsrm(strs []string, val string) ([]string, bool) { + removed := false + res := []string{} + for _, str := range strs { + if str == val { + removed = true + continue + } + res = append(res, str) + } + return res, removed +} + +func strsadd(strs []string, val string) ([]string, bool) { + for _, str := range strs { + if str == val { + return strs, false + } + } + return append(strs, val), true +} diff --git a/gno/p/daocond/gno.mod b/gno/p/daocond/gno.mod new file mode 100644 index 0000000000..7eb4bedf23 --- /dev/null +++ b/gno/p/daocond/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/daocond From eb8745e3b31a2408e8611ac4104a87f8b235b2e8 Mon Sep 17 00:00:00 2001 From: clegirar <33428384+clegirar@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:24:38 +0100 Subject: [PATCH 2/2] fix: fontWeight and fullWidth for Wallet manager screen (#1473) Signed-off-by: clegirar --- packages/components/Menu.tsx | 4 +- packages/components/buttons/MaxButton.tsx | 4 +- packages/components/hub/MyNFTs.tsx | 29 +++----- .../components/hub/WalletDashboardHeader.tsx | 55 ++++------------ .../modals/DepositWithdrawModal.tsx | 25 ++++--- packages/screens/WalletManager/Assets.tsx | 41 ++++-------- packages/screens/WalletManager/WalletItem.tsx | 66 ++++++------------- .../WalletManager/WalletManagerScreen.tsx | 9 ++- .../screens/WalletManager/WalletsScreen.tsx | 16 ++--- packages/utils/style/fonts.ts | 7 ++ 10 files changed, 93 insertions(+), 163 deletions(-) diff --git a/packages/components/Menu.tsx b/packages/components/Menu.tsx index 5b4b85e7c4..92534995ca 100644 --- a/packages/components/Menu.tsx +++ b/packages/components/Menu.tsx @@ -6,7 +6,7 @@ import { PrimaryBox } from "./boxes/PrimaryBox"; import { useDropdowns } from "@/hooks/useDropdowns"; import { neutral33 } from "@/utils/style/colors"; -import { fontSemibold13 } from "@/utils/style/fonts"; +import { fontRegular13 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; const DEFAULT_WIDTH = 164; @@ -62,7 +62,7 @@ export const Menu: React.FC = ({ ]} > {item.label} diff --git a/packages/components/buttons/MaxButton.tsx b/packages/components/buttons/MaxButton.tsx index 3eff1c6978..d99c9ebce0 100644 --- a/packages/components/buttons/MaxButton.tsx +++ b/packages/components/buttons/MaxButton.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Pressable, StyleSheet } from "react-native"; import { neutral22, primaryColor } from "../../utils/style/colors"; -import { fontSemibold12 } from "../../utils/style/fonts"; +import { fontRegular12 } from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; import { BrandText } from "../BrandText"; @@ -22,7 +22,7 @@ export const MaxButton = ({ onPress }: MaxButtonProps) => { // eslint-disable-next-line no-restricted-syntax const styles = StyleSheet.create({ maxText: { - ...StyleSheet.flatten(fontSemibold12), + ...StyleSheet.flatten(fontRegular12), backgroundColor: primaryColor, color: neutral22, borderRadius: layout.borderRadius, diff --git a/packages/components/hub/MyNFTs.tsx b/packages/components/hub/MyNFTs.tsx index 42f6102a1e..ee411c6201 100644 --- a/packages/components/hub/MyNFTs.tsx +++ b/packages/components/hub/MyNFTs.tsx @@ -10,6 +10,8 @@ import { OmniLink } from "../OmniLink"; import { SVG } from "../SVG"; import { NFTView } from "../nfts/NFTView"; +import { fontRegular14, fontRegular20 } from "@/utils/style/fonts"; + export const MyNFTs: React.FC = () => { const selectedWallet = useSelectedWallet(); @@ -25,11 +27,7 @@ export const MyNFTs: React.FC = () => { priceRange: undefined, }); return ( - + { marginBottom: 24, }} > - My NFTs + + My NFTs + - + See All - + = ({ = ({ position: "relative", }} > - - {title} - - - {data} - + {title} + {data} {!!actionButton && ( )} @@ -129,12 +115,7 @@ export const WalletDashboardHeader: React.FC = () => { marginTop: -layout.spacing_x3, }} > - + { - + Hello {userInfo.metadata?.tokenId || diff --git a/packages/components/modals/DepositWithdrawModal.tsx b/packages/components/modals/DepositWithdrawModal.tsx index 8056d5e59a..340be21ed0 100644 --- a/packages/components/modals/DepositWithdrawModal.tsx +++ b/packages/components/modals/DepositWithdrawModal.tsx @@ -18,7 +18,11 @@ import { keplrCurrencyFromNativeCurrencyInfo, } from "../../networks"; import { neutral77, primaryColor } from "../../utils/style/colors"; -import { fontSemibold13, fontSemibold14 } from "../../utils/style/fonts"; +import { + fontRegular13, + fontRegular14, + fontRegular16, +} from "../../utils/style/fonts"; import { layout } from "../../utils/style/layout"; import { capitalize, tinyAddress } from "../../utils/text"; import { BrandText } from "../BrandText"; @@ -83,7 +87,7 @@ export const DepositWithdrawModal: React.FC = ({ - + {variation === "deposit" ? "Deposit on" : "Withdraw from"}{" "} {getNetwork(networkId)?.displayName || "Unknown"} @@ -110,7 +114,7 @@ export const DepositWithdrawModal: React.FC = ({ width={460} > - + {capitalize(variation)} {nativeTargetCurrency?.displayName} @@ -120,7 +124,7 @@ export const DepositWithdrawModal: React.FC = ({ - + From {sourceNetwork?.displayName || "Unknown"} @@ -149,7 +153,7 @@ export const DepositWithdrawModal: React.FC = ({ /> - + To {destinationNetwork?.displayName || "Unknown"} @@ -175,9 +179,9 @@ export const DepositWithdrawModal: React.FC = ({ rules={{ required: true, max }} placeHolder="0" subtitle={ - + Available:{" "} - + {max} @@ -329,12 +333,7 @@ const styles = StyleSheet.create({ container: { paddingBottom: layout.spacing_x3, }, - estimatedText: StyleSheet.flatten([ - fontSemibold14, - { - color: neutral77, - }, - ]), + estimatedText: StyleSheet.flatten([fontRegular14, { color: neutral77 }]), }); const convertCosmosAddress = ( diff --git a/packages/screens/WalletManager/Assets.tsx b/packages/screens/WalletManager/Assets.tsx index 4a4e70d736..b38c18536a 100644 --- a/packages/screens/WalletManager/Assets.tsx +++ b/packages/screens/WalletManager/Assets.tsx @@ -14,6 +14,11 @@ import { useIsMobile } from "@/hooks/useIsMobile"; import { parseUserId } from "@/networks"; import { prettyPrice } from "@/utils/coins"; import { neutral22, neutral33 } from "@/utils/style/colors"; +import { + fontRegular14, + fontRegular18, + fontRegular20, +} from "@/utils/style/fonts"; const collapsedCount = 5; @@ -81,7 +86,7 @@ export const Assets: React.FC<{ alignItems: "center", }} > - + Assets on {network.displayName} @@ -92,13 +97,7 @@ export const Assets: React.FC<{ alignItems: "center", }} > - + {expanded ? "Collapse" : "Expand"} All Items - + - + {prettyPrice( network.id, balance?.amount || "0", currency.denom, )} - + {balance?.usdAmount ? `≈ $${balance.usdAmount.toFixed(2)}` : " "} - + {!readOnly && currency.kind === "ibc" && ( <> {currency.deprecated || ( diff --git a/packages/screens/WalletManager/WalletItem.tsx b/packages/screens/WalletManager/WalletItem.tsx index 94d616d8c3..d1a96acdb4 100644 --- a/packages/screens/WalletManager/WalletItem.tsx +++ b/packages/screens/WalletManager/WalletItem.tsx @@ -29,6 +29,12 @@ import { } from "@/store/slices/settings"; import { useAppDispatch } from "@/store/store"; import { neutral33, neutral77 } from "@/utils/style/colors"; +import { + fontRegular12, + fontRegular14, + fontRegular16, + fontRegular18, +} from "@/utils/style/fonts"; import { modalMarginPadding } from "@/utils/style/modals"; interface WalletItemProps { @@ -75,12 +81,7 @@ export const WalletItem: React.FC = ({ zIndex, }} > - + {selectable && ( = ({ - {item.provider} + {item.provider} = ({ marginTop: 8, }} > - - {item.address} - + {item.address} { Clipboard.setStringAsync(item.address); @@ -149,12 +144,7 @@ export const WalletItem: React.FC = ({ - + = ({ }} > Staked - + {`$${delegationsUsdBalance.toFixed(2)}`} - + Pending rewards - + {`$${claimableUSD.toFixed(2)}`} @@ -272,7 +244,7 @@ const DetailsModal: React.FC<{ visible={visible} onClose={onClose} > - + {JSON.stringify(wallet, null, 4)} diff --git a/packages/screens/WalletManager/WalletManagerScreen.tsx b/packages/screens/WalletManager/WalletManagerScreen.tsx index 959f45fae7..21b0a4dee2 100644 --- a/packages/screens/WalletManager/WalletManagerScreen.tsx +++ b/packages/screens/WalletManager/WalletManagerScreen.tsx @@ -15,19 +15,22 @@ import { useAreThereWallets } from "@/hooks/useAreThereWallets"; import { useMaxResolution } from "@/hooks/useMaxResolution"; import { ScreenFC } from "@/utils/navigation"; import { neutral33 } from "@/utils/style/colors"; +import { fontRegular20 } from "@/utils/style/fonts"; import { layout } from "@/utils/style/layout"; export const WalletManagerScreen: ScreenFC<"WalletManager"> = () => { const selectedWallet = useSelectedWallet(); const areThereWallets = useAreThereWallets(); - const { height } = useMaxResolution(); + const { height, width } = useMaxResolution({ isLarge: true }); return ( - }> + }> {areThereWallets ? ( @@ -49,7 +52,7 @@ export const WalletManagerScreen: ScreenFC<"WalletManager"> = () => { zIndex: 99, }} > - + Wallet {!!selectedWallet && ( diff --git a/packages/screens/WalletManager/WalletsScreen.tsx b/packages/screens/WalletManager/WalletsScreen.tsx index 56fdf2ec09..811a66eebd 100644 --- a/packages/screens/WalletManager/WalletsScreen.tsx +++ b/packages/screens/WalletManager/WalletsScreen.tsx @@ -10,20 +10,25 @@ import { ScreenContainer } from "@/components/ScreenContainer"; import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { ConnectWalletModal } from "@/components/modals/ConnectWalletModal"; import { useWallets } from "@/context/WalletsProvider"; +import { useMaxResolution } from "@/hooks/useMaxResolution"; import { ScreenFC } from "@/utils/navigation"; import { neutral33, neutralA3 } from "@/utils/style/colors"; +import { fontRegular14, fontRegular20 } from "@/utils/style/fonts"; export const WalletManagerWalletsScreen: ScreenFC< "WalletManagerWallets" | "WalletManagerChains" > = () => { const [showConnectModal, setShowConnectModal] = useState(false); const { wallets: allWallets } = useWallets(); + const { width } = useMaxResolution({ isLarge: true }); return ( - }> + }> - + All Wallets - + Manage your wallets diff --git a/packages/utils/style/fonts.ts b/packages/utils/style/fonts.ts index 662240ad18..4b791bbc77 100644 --- a/packages/utils/style/fonts.ts +++ b/packages/utils/style/fonts.ts @@ -243,6 +243,13 @@ export const fontRegular20: TextStyle = { fontFamily: "Exo_400Regular", fontWeight: "400", }; +export const fontRegular18: TextStyle = { + fontSize: 18, + letterSpacing: -(18 * 0.02), + lineHeight: 20, + fontFamily: "Exo_400Regular", + fontWeight: "400", +}; export const fontRegular16: TextStyle = { fontSize: 16, letterSpacing: -(16 * 0.02),