Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}