basedao.gno
6.95 Kb · 250 lines
1package basedao
2
3import (
4 "chain"
5 "chain/runtime"
6 "chain/runtime/unsafe"
7 "errors"
8
9 "gno.land/p/nt/mux/v0"
10 "gno.land/p/samcrew/daocond"
11 "gno.land/p/samcrew/daokit"
12 "gno.land/p/samcrew/realmid"
13)
14
15type daoPublic struct {
16 impl *DAOPrivate
17}
18
19type SetImplemRaw func(newDAO daokit.DAO)
20
21type RenderFn func(path string, dao *DAOPrivate) string
22
23func (d *daoPublic) Extension(path string) daokit.Extension {
24 ext, ok := d.impl.Core.Extensions.Get(path)
25 if !ok {
26 return nil
27 }
28 if ext.Info().Private && unsafe.CurrentRealm().PkgPath() != d.impl.Realm.PkgPath() {
29 panic("attempt to get private extension outside of DAO realm")
30 }
31 return ext
32}
33
34func (d *daoPublic) ExtensionsList() daokit.ExtensionsList {
35 return d.impl.Core.Extensions.List()
36}
37
38func (d *daoPublic) Propose(req daokit.ProposalRequest) uint64 {
39 return d.impl.Propose(req)
40}
41
42func (d *daoPublic) Execute(id uint64, rlm realm) {
43 d.impl.Execute(id, rlm)
44}
45
46func (d *daoPublic) Vote(id uint64, vote daocond.Vote) {
47 d.impl.Vote(id, vote)
48}
49
50func (d *daoPublic) Render(path string) string {
51 return d.impl.Render(path)
52}
53
54// DAO is meant for internal realm usage and should not be exposed.
55type DAOPrivate struct {
56 Core *daokit.Core
57 Members *MembersStore
58 RenderRouter *mux.Router
59 GetProfileString ProfileStringGetter
60 Realm runtime.Realm
61 RenderFn func(path string, dao *DAOPrivate) string
62 CallerID CallerIDFn
63
64 InitialConfig *Config // mostly there in case we need this data during upgrade
65}
66
67// Function type that returns the identifier of the current caller.
68// Used to identify who is making calls to DAO functions.
69type CallerIDFn func() string
70
71type Config struct {
72 // Basic DAO information
73 Name string
74 Description string
75 ImageURI string
76
77 // Storage handler
78 Members *MembersStore
79
80 // Feature toggles
81 NoDefaultHandlers bool // Skips registration of default management actions (add/remove members, etc.)
82 NoDefaultRendering bool // Skips setup of default web UI rendering routes
83 NoCreationEvent bool // Skips emitting the DAO creation event
84
85 // Governance configuration
86 InitialCondition daocond.Condition // Default condition for all built-in actions, defaults to 60% member majority
87
88 // Profile integration (optional)
89 SetProfileString ProfileStringSetter // Function to update profile fields (DisplayName, Bio, Avatar)
90 GetProfileString ProfileStringGetter // Function to retrieve profile fields for members
91
92 // Advanced customization hooks
93 SetImplemFn SetImplemRaw // Function called when DAO implementation changes via governance
94 MigrationParamsFn MigrationParamsFn // Function providing parameters for DAO upgrades
95 RenderFn RenderFn // Rendering function for Gnoweb
96 CallerID CallerIDFn // Custom function to identify the current caller, defaults to realmid.Previous
97
98 // Internal configuration
99 PrivateVarName string // Name of the private DAO variable for member querying extensions
100}
101
102type ProfileStringSetter func(cur realm, field string, value string) bool
103type ProfileStringGetter func(addr address, field string, def string) string
104
105const EventBaseDAOCreated = "BaseDAOCreated"
106
107func New(conf *Config, rlm realm) (daokit.DAO, *DAOPrivate) {
108 // XXX: emit events from memberstore
109
110 members := conf.Members
111 if members == nil {
112 members = NewMembersStore(nil, nil)
113 }
114
115 if conf.GetProfileString == nil {
116 panic(errors.New("GetProfileString is required"))
117 }
118
119 if conf.CallerID == nil {
120 conf.CallerID = realmid.Previous
121 }
122
123 core := daokit.NewCore()
124 dao := &DAOPrivate{
125 Core: core,
126 Members: members,
127 GetProfileString: conf.GetProfileString,
128 Realm: unsafe.CurrentRealm(),
129 RenderFn: conf.RenderFn,
130 CallerID: conf.CallerID,
131 InitialConfig: conf,
132 }
133
134 pubdao := &daoPublic{impl: dao}
135
136 dao.Core.Extensions.Set(&membersViewExtension{
137 getStore: func() *MembersStore { return dao.Members },
138 queryPath: dao.Realm.PkgPath() + "." + conf.PrivateVarName + ".Members",
139 })
140
141 dao.initRenderingRouter()
142
143 if !conf.NoDefaultRendering {
144 dao.InitDefaultRendering()
145 }
146
147 if conf.SetProfileString != nil {
148 conf.SetProfileString(cross(rlm), "DisplayName", conf.Name)
149 conf.SetProfileString(cross(rlm), "Bio", conf.Description)
150 conf.SetProfileString(cross(rlm), "Avatar", conf.ImageURI)
151 }
152
153 if !conf.NoDefaultHandlers {
154 if conf.InitialCondition == nil {
155 conf.InitialCondition = daocond.MembersThreshold(0.6, members.IsMember, members.MembersCount)
156 }
157
158 if conf.SetProfileString != nil {
159 dao.Core.Resources.Set(&daokit.Resource{
160 Handler: NewEditProfileHandler(conf.SetProfileString, []string{"DisplayName", "Bio", "Avatar"}),
161 Condition: conf.InitialCondition,
162 DisplayName: "Edit Profile",
163 Description: "This proposal allows you to edit this DAO profile.",
164 })
165 }
166
167 if conf.SetImplemFn != nil {
168 setImplemFn := func(dao daokit.DAO) {
169 conf.SetImplemFn(dao)
170 }
171 setImplemFn(pubdao)
172
173 if conf.MigrationParamsFn == nil {
174 conf.MigrationParamsFn = func() []any { return nil }
175 }
176
177 dao.Core.Resources.Set(&daokit.Resource{
178 Handler: NewChangeDAOImplementationHandler(dao, setImplemFn, conf.MigrationParamsFn),
179 Condition: conf.InitialCondition,
180 DisplayName: "Change DAO Implementation",
181 Description: "Change the underlying DAO implementation.",
182 })
183 }
184
185 defaultResources := []daokit.Resource{
186 {
187 Handler: NewAddMemberHandler(dao),
188 Condition: conf.InitialCondition,
189 DisplayName: "Add Member",
190 Description: "This proposal allows you to add a new member to the DAO.",
191 },
192 {
193 Handler: NewRemoveMemberHandler(dao),
194 Condition: conf.InitialCondition,
195 DisplayName: "Remove Member",
196 Description: "This proposal allows you to remove a member from the DAO.",
197 },
198 {
199 Handler: NewAssignRoleHandler(dao),
200 Condition: conf.InitialCondition,
201 DisplayName: "Assign Role",
202 Description: "This proposal allows you to assign a role to a member.",
203 },
204 {
205 Handler: NewUnassignRoleHandler(dao),
206 Condition: conf.InitialCondition,
207 DisplayName: "Unassign Role",
208 Description: "This proposal allows you to unassign a role from a member.",
209 },
210 }
211 // register management handlers
212 for _, resource := range defaultResources {
213 dao.Core.Resources.Set(&resource)
214 }
215
216 }
217
218 if !conf.NoCreationEvent {
219 chain.Emit(EventBaseDAOCreated)
220 }
221
222 return pubdao, dao
223}
224
225func (d *DAOPrivate) Vote(proposalID uint64, vote daocond.Vote) {
226 if len(vote) > 16 {
227 panic("invalid vote")
228 }
229
230 voterID := d.assertCallerIsMember()
231 d.Core.Vote(voterID, proposalID, vote)
232}
233
234func (d *DAOPrivate) Execute(proposalID uint64, rlm realm) {
235 _ = d.assertCallerIsMember()
236 d.Core.Execute(proposalID, rlm)
237}
238
239func (d *DAOPrivate) Propose(req daokit.ProposalRequest) uint64 {
240 proposerID := d.assertCallerIsMember()
241 return d.Core.Propose(proposerID, req)
242}
243
244func (d *DAOPrivate) assertCallerIsMember() string {
245 id := d.CallerID()
246 if !d.Members.IsMember(id) {
247 panic(errors.New("caller is not a member"))
248 }
249 return id
250}