From ec523e7c0e4d279fe839f4f2213281cde7f041dd Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Mon, 18 Sep 2023 23:27:02 +0200 Subject: [PATCH 01/73] feat: DA0-DA0 port Signed-off-by: Norman Meier --- .gitignore | 1 + .../gno.land/p/demo/daodao/core/dao_core.gno | 228 ++++ .../p/demo/daodao/core/dao_core_test.gno | 172 +++ .../gno.land/p/demo/daodao/core/errors.gno | 15 + .../p/demo/daodao/core/expiration.gno | 47 + .../p/demo/daodao/core/expiration_test.gno | 15 + examples/gno.land/p/demo/daodao/core/gno.mod | 7 + .../gno.land/p/demo/daodao/core/messages.gno | 93 ++ .../p/demo/daodao/interfaces/core.gno | 18 + .../gno.land/p/demo/daodao/interfaces/gno.mod | 6 + .../p/demo/daodao/interfaces/messages.gno | 26 + .../daodao/interfaces/messages_registry.gno | 143 +++ .../interfaces/messages_registry_test.gno | 52 + .../daodao/interfaces/messages_testing.gno | 54 + .../p/demo/daodao/interfaces/modules.gno | 37 + .../proposal_single/dao_proposal_single.gno | 379 +++++++ .../p/demo/daodao/proposal_single/gno.mod | 7 + .../daodao/proposal_single/proposal_test.gno | 64 ++ .../demo/daodao/proposal_single/threshold.gno | 151 +++ .../p/demo/daodao/proposal_single/types.gno | 188 ++++ .../proposal_single/update_settings.gno | 80 ++ .../p/demo/daodao/voting_group/gno.mod | 7 + .../demo/daodao/voting_group/voting_group.gno | 42 + .../daodao/voting_group/voting_group_test.gno | 21 + .../p/demo/flags_index/flags_index.gno | 162 +++ examples/gno.land/p/demo/flags_index/gno.mod | 5 + examples/gno.land/p/demo/havl/gno.mod | 5 + examples/gno.land/p/demo/havl/havl.gno | 128 +++ .../gno.land/p/demo/markdown_utils/gno.mod | 1 + .../p/demo/markdown_utils/markdown_utils.gno | 26 + examples/gno.land/p/demo/ujson/format.gno | 137 +++ examples/gno.land/p/demo/ujson/gno.mod | 7 + examples/gno.land/p/demo/ujson/parse.gno | 589 +++++++++++ examples/gno.land/p/demo/ujson/strings.gno | 233 ++++ examples/gno.land/p/demo/ujson/tables.gno | 216 ++++ examples/gno.land/p/demo/ujson/ujson_test.gno | 178 ++++ examples/gno.land/p/demo/utf16/gno.mod | 1 + examples/gno.land/p/demo/utf16/utf16.gno | 108 ++ .../gno.land/r/demo/dao_realm/dao_realm.gno | 125 +++ .../r/demo/dao_realm/dao_realm_test.gno | 157 +++ examples/gno.land/r/demo/dao_realm/gno.mod | 13 + .../r/demo/dao_registry/dao_registry.gno | 104 ++ .../r/demo/dao_registry/dao_registry_test.gno | 54 + examples/gno.land/r/demo/dao_registry/gno.mod | 6 + examples/gno.land/r/demo/groups/gno.mod | 4 + examples/gno.land/r/demo/groups/group.gno | 68 +- examples/gno.land/r/demo/groups/member.gno | 37 + examples/gno.land/r/demo/groups/messages.gno | 124 +++ .../gno.land/r/demo/groups/messages_test.gno | 40 + examples/gno.land/r/demo/groups/public.gno | 80 +- .../gno.land/r/demo/groups/z_0_a_filetest.gno | 7 +- .../gno.land/r/demo/groups/z_1_a_filetest.gno | 4 +- .../gno.land/r/demo/groups/z_1_c_filetest.gno | 4 +- .../gno.land/r/demo/groups/z_2_a_filetest.gno | 4 +- .../gno.land/r/demo/groups/z_2_d_filetest.gno | 7 +- .../gno.land/r/demo/groups/z_2_g_filetest.gno | 5 +- examples/gno.land/r/demo/modboards/README.md | 136 +++ examples/gno.land/r/demo/modboards/board.gno | 177 ++++ examples/gno.land/r/demo/modboards/boards.gno | 22 + .../gno.land/r/demo/modboards/example_post.md | 3 + examples/gno.land/r/demo/modboards/flags.gno | 28 + examples/gno.land/r/demo/modboards/gno.mod | 9 + .../gno.land/r/demo/modboards/messages.gno | 149 +++ examples/gno.land/r/demo/modboards/misc.gno | 96 ++ examples/gno.land/r/demo/modboards/post.gno | 268 +++++ examples/gno.land/r/demo/modboards/public.gno | 193 ++++ examples/gno.land/r/demo/modboards/render.gno | 92 ++ examples/gno.land/r/demo/modboards/role.gno | 8 + .../r/demo/modboards/z_0_a_filetest.gno | 22 + .../r/demo/modboards/z_0_b_filetest.gno | 23 + .../r/demo/modboards/z_0_c_filetest.gno | 23 + .../r/demo/modboards/z_0_d_filetest.gno | 24 + .../r/demo/modboards/z_0_e_filetest.gno | 23 + .../r/demo/modboards/z_0_filetest.gno | 39 + .../r/demo/modboards/z_10_a_filetest.gno | 34 + .../r/demo/modboards/z_10_b_filetest.gno | 34 + .../r/demo/modboards/z_10_c_filetest.gno | 48 + .../r/demo/modboards/z_10_filetest.gno | 39 + .../r/demo/modboards/z_11_a_filetest.gno | 34 + .../r/demo/modboards/z_11_b_filetest.gno | 34 + .../r/demo/modboards/z_11_c_filetest.gno | 34 + .../r/demo/modboards/z_11_d_filetest.gno | 52 + .../r/demo/modboards/z_11_filetest.gno | 42 + .../r/demo/modboards/z_1_filetest.gno | 28 + .../r/demo/modboards/z_2_filetest.gno | 38 + .../r/demo/modboards/z_3_filetest.gno | 40 + .../r/demo/modboards/z_4_filetest.gno | 997 ++++++++++++++++++ .../r/demo/modboards/z_5_b_filetest.gno | 31 + .../r/demo/modboards/z_5_c_filetest.gno | 39 + .../r/demo/modboards/z_5_d_filetest.gno | 32 + .../r/demo/modboards/z_5_filetest.gno | 43 + .../r/demo/modboards/z_6_filetest.gno | 49 + .../r/demo/modboards/z_7_filetest.gno | 31 + .../r/demo/modboards/z_8_filetest.gno | 44 + .../r/demo/modboards/z_9_a_filetest.gno | 27 + .../r/demo/modboards/z_9_b_filetest.gno | 31 + .../r/demo/modboards/z_9_filetest.gno | 37 + examples/gno.land/r/demo/tori/gno.mod | 8 + examples/gno.land/r/demo/tori/messages.gno | 108 ++ examples/gno.land/r/demo/tori/tori.gno | 112 ++ gnovm/docs/go-gno-compatibility.md | 2 +- gnovm/pkg/gnolang/values.go | 2 +- gnovm/stdlibs/unicode/utf16/export_test.gno | 11 + gnovm/stdlibs/unicode/utf16/utf16.gno | 108 ++ gnovm/stdlibs/unicode/utf16/utf16_test.gno | 208 ++++ 105 files changed, 8165 insertions(+), 39 deletions(-) create mode 100644 examples/gno.land/p/demo/daodao/core/dao_core.gno create mode 100644 examples/gno.land/p/demo/daodao/core/dao_core_test.gno create mode 100644 examples/gno.land/p/demo/daodao/core/errors.gno create mode 100644 examples/gno.land/p/demo/daodao/core/expiration.gno create mode 100644 examples/gno.land/p/demo/daodao/core/expiration_test.gno create mode 100644 examples/gno.land/p/demo/daodao/core/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/core/messages.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/core.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces/messages.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/messages_registry.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/messages_registry_test.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/messages_testing.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces/modules.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/dao_proposal_single.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/proposal_test.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/threshold.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/types.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single/update_settings.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/voting_group/voting_group.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group/voting_group_test.gno create mode 100644 examples/gno.land/p/demo/flags_index/flags_index.gno create mode 100644 examples/gno.land/p/demo/flags_index/gno.mod create mode 100644 examples/gno.land/p/demo/havl/gno.mod create mode 100644 examples/gno.land/p/demo/havl/havl.gno create mode 100644 examples/gno.land/p/demo/markdown_utils/gno.mod create mode 100644 examples/gno.land/p/demo/markdown_utils/markdown_utils.gno create mode 100644 examples/gno.land/p/demo/ujson/format.gno create mode 100644 examples/gno.land/p/demo/ujson/gno.mod create mode 100644 examples/gno.land/p/demo/ujson/parse.gno create mode 100644 examples/gno.land/p/demo/ujson/strings.gno create mode 100644 examples/gno.land/p/demo/ujson/tables.gno create mode 100644 examples/gno.land/p/demo/ujson/ujson_test.gno create mode 100644 examples/gno.land/p/demo/utf16/gno.mod create mode 100644 examples/gno.land/p/demo/utf16/utf16.gno create mode 100644 examples/gno.land/r/demo/dao_realm/dao_realm.gno create mode 100644 examples/gno.land/r/demo/dao_realm/dao_realm_test.gno create mode 100644 examples/gno.land/r/demo/dao_realm/gno.mod create mode 100644 examples/gno.land/r/demo/dao_registry/dao_registry.gno create mode 100644 examples/gno.land/r/demo/dao_registry/dao_registry_test.gno create mode 100644 examples/gno.land/r/demo/dao_registry/gno.mod create mode 100644 examples/gno.land/r/demo/groups/messages.gno create mode 100644 examples/gno.land/r/demo/groups/messages_test.gno create mode 100644 examples/gno.land/r/demo/modboards/README.md create mode 100644 examples/gno.land/r/demo/modboards/board.gno create mode 100644 examples/gno.land/r/demo/modboards/boards.gno create mode 100644 examples/gno.land/r/demo/modboards/example_post.md create mode 100644 examples/gno.land/r/demo/modboards/flags.gno create mode 100644 examples/gno.land/r/demo/modboards/gno.mod create mode 100644 examples/gno.land/r/demo/modboards/messages.gno create mode 100644 examples/gno.land/r/demo/modboards/misc.gno create mode 100644 examples/gno.land/r/demo/modboards/post.gno create mode 100644 examples/gno.land/r/demo/modboards/public.gno create mode 100644 examples/gno.land/r/demo/modboards/render.gno create mode 100644 examples/gno.land/r/demo/modboards/role.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_a_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_b_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_c_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_d_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_e_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_0_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_10_a_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_10_b_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_10_c_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_10_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_11_a_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_11_b_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_11_c_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_11_d_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_11_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_1_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_2_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_3_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_4_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_5_b_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_5_c_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_5_d_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_5_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_6_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_7_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_8_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_9_a_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_9_b_filetest.gno create mode 100644 examples/gno.land/r/demo/modboards/z_9_filetest.gno create mode 100644 examples/gno.land/r/demo/tori/gno.mod create mode 100644 examples/gno.land/r/demo/tori/messages.gno create mode 100644 examples/gno.land/r/demo/tori/tori.gno create mode 100644 gnovm/stdlibs/unicode/utf16/export_test.gno create mode 100644 gnovm/stdlibs/unicode/utf16/utf16.gno create mode 100644 gnovm/stdlibs/unicode/utf16/utf16_test.gno diff --git a/.gitignore b/.gitignore index d7e6d7eb9a4..6f6c5ec254d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ pbbindings.go *# cover.out coverage.out +/.deploy/ \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/core/dao_core.gno b/examples/gno.land/p/demo/daodao/core/dao_core.gno new file mode 100644 index 00000000000..8a8160cfc9c --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/dao_core.gno @@ -0,0 +1,228 @@ +package core + +import ( + "std" + "strings" + "time" + + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/markdown_utils" +) + +// TODO: clean this file + +// TODO: add wrapper message handler to handle multiple proposal modules messages + +type daoCore struct { + dao_interfaces.IDAOCore + + votingModule dao_interfaces.IVotingModule + proposalModules []dao_interfaces.ActivableProposalModule + activeProposalModuleCount int + realm std.Realm + registry *dao_interfaces.MessagesRegistry +} + +func NewDAOCore( + votingModuleFactory dao_interfaces.VotingModuleFactory, + proposalModulesFactories []dao_interfaces.ProposalModuleFactory, + messageHandlersFactories []dao_interfaces.MessageHandlerFactory, +) dao_interfaces.IDAOCore { + if votingModuleFactory == nil { + panic("Missing voting module factory") + } + + if len(proposalModulesFactories) == 0 { + panic("No proposal modules factories") + } + + core := &daoCore{ + realm: std.CurrentRealm(), + activeProposalModuleCount: len(proposalModulesFactories), + registry: dao_interfaces.NewMessagesRegistry(), + proposalModules: make([]dao_interfaces.ActivableProposalModule, len(proposalModulesFactories)), + } + + core.votingModule = votingModuleFactory(core) + if core.votingModule == nil { + panic("voting module factory returned nil") + } + + for i, modFactory := range proposalModulesFactories { + mod := modFactory(core) + if mod == nil { + panic("proposal module factory returned nil") + } + core.proposalModules[i] = dao_interfaces.ActivableProposalModule{ + Enabled: true, + Module: mod, + } + } + + // this registry is specific to gno since we can't do dynamic calls + core.registry.Register(NewUpdateVotingModuleMessageHandler(core)) + core.registry.Register(NewUpdateProposalModulesMessageHandler(core)) + for _, handlerFactory := range messageHandlersFactories { + handler := handlerFactory(core) + if handler == nil { + panic("message handler factory returned nil") + } + core.registry.Register(handler) + } + + return core +} + +// mutations + +func (d *daoCore) ExecuteProposalHook(moduleIndex int, msgs []dao_interfaces.ExecutableMessage) { + module := GetProposalModule(d, moduleIndex) + if !module.Enabled { + panic(ErrModuleDisabledCannotExecute) + } + + d.executeMsgs(msgs) +} + +func (d *daoCore) UpdateVotingModule(newVotingModule dao_interfaces.IVotingModule) { + if std.CurrentRealm().Addr() != d.realm.Addr() { // not sure this check necessary since the ownership system should protect against mutation from other realms + panic(ErrUnauthorized) + } + + // FIXME: check da0-da0 implem + d.votingModule = newVotingModule +} + +func (d *daoCore) UpdateProposalModules(toAdd []dao_interfaces.IProposalModule, toDisable []int) { + if std.CurrentRealm().Addr() != d.realm.Addr() { // not sure this check necessary since the ownership system should protect against mutation from other realms + panic(ErrUnauthorized) + } + + for _, module := range toAdd { + d.AddProposalModule(module) + } + + for _, moduleIndex := range toDisable { + module := GetProposalModule(d, moduleIndex) + + if !module.Enabled { + panic(ErrModuleAlreadyDisabled) + } + module.Enabled = false + + d.activeProposalModuleCount-- + if d.activeProposalModuleCount == 0 { + panic("no active proposal modules") // this -> `panic(ErrNoActiveProposalModules)` triggers `panic: reflect: reflect.Value.SetString using value obtained using unexported field` + } + } +} + +// gno-specific mutations + +func (d *daoCore) RegisterMessageHandler(msg dao_interfaces.MessageHandler) { + d.registry.Register(msg) +} + +func (d *daoCore) RemoveMessageHandler(t string) { + d.registry.Remove(t) +} + +// queries + +func (d *daoCore) DumpState() { + panic(ErrNotImplemented) +} + +func (d *daoCore) Info() { + panic(ErrNotImplemented) +} + +func (d *daoCore) ProposalModules() []dao_interfaces.ActivableProposalModule { + return d.proposalModules +} + +func (d *daoCore) ProposalModuleCount() int { + return len(d.proposalModules) +} + +func (d *daoCore) TotalPowerAtHeight() { + panic(ErrNotImplemented) +} + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) VotingPowerAtHeight(address std.Address, height int64) uint64 { + return d.VotingModule().VotingPowerAtHeight(address, height) +} + +func (d *daoCore) ActiveProposalModules() { + panic(ErrNotImplemented) +} + +// custom queries + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) AddProposalModule(proposalMod dao_interfaces.IProposalModule) { + for _, mod := range d.proposalModules { + if mod.Module != proposalMod { + continue + } + + if mod.Enabled { + panic(ErrModuleAlreadyAdded) + } + mod.Enabled = true + d.activeProposalModuleCount++ + return + } + + d.proposalModules = append(d.proposalModules, dao_interfaces.ActivableProposalModule{ + Enabled: true, + Module: proposalMod, + }) + + d.activeProposalModuleCount++ +} + +func (d *daoCore) ActiveProposalModuleCount() int { + return d.activeProposalModuleCount +} + +func (d *daoCore) Render(path string) string { + s := "# DAO Core\n" + s += "This is an attempt at porting [DA0-DA0 contracts](https://github.com/DA0-DA0/dao-contracts)\n" + s += markdown_utils.Indent(d.votingModule.Render(path)) + "\n" + for _, propMod := range d.proposalModules { + if !propMod.Enabled { + continue + } + s += markdown_utils.Indent(propMod.Module.Render(path)) + "\n" + } + return s +} + +func (d *daoCore) Registry() *dao_interfaces.MessagesRegistry { + return d.registry +} + +func GetProposalModule(core dao_interfaces.IDAOCore, moduleIndex int) *dao_interfaces.ActivableProposalModule { + if moduleIndex < 0 { + panic("module index must be >= 0") + } + mods := core.ProposalModules() + if moduleIndex >= len(mods) { + panic("invalid module index") + } + return &mods[moduleIndex] +} + +func (d *daoCore) executeMsgs(msgs []dao_interfaces.ExecutableMessage) { + for _, msg := range msgs { + d.registry.Execute(msg) + } +} diff --git a/examples/gno.land/p/demo/daodao/core/dao_core_test.gno b/examples/gno.land/p/demo/daodao/core/dao_core_test.gno new file mode 100644 index 00000000000..7ca53b09180 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/dao_core_test.gno @@ -0,0 +1,172 @@ +package core + +import ( + "std" + "testing" + + dao_interfaces "gno.land/p/demo/daodao/interfaces" +) + +type votingModule struct { + core dao_interfaces.IDAOCore +} + +func votingModuleFactory(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + return &votingModule{core: core} +} + +func (vm *votingModule) Core() dao_interfaces.IDAOCore { + return vm.core +} + +func (vm *votingModule) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "TestVoting", + Version: "21.42", + } +} + +func (vm *votingModule) Render(path string) string { + return "# Test Voting Module" +} + +func (vm *votingModule) VotingPowerAtHeight(address std.Address, height int64) uint64 { + return 0 +} + +func (vm *votingModule) TotalPowerAtHeight(height int64) uint64 { + return 0 +} + +type proposalModule struct { + core dao_interfaces.IDAOCore +} + +func proposalModuleFactory(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + return &proposalModule{core: core} +} + +func (pm *proposalModule) Core() dao_interfaces.IDAOCore { + return pm.core +} + +func (pm *proposalModule) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "TestProposal", + Version: "42.21", + } +} + +func (pm *proposalModule) VoteJSON(proposalID int, voteJSON string) { + panic("not implemented") +} + +func (pm *proposalModule) Render(path string) string { + return "# Test Proposal Module" +} + +func (pm *proposalModule) Execute(proposalId int) { + panic("not implemented") +} + +func (pm *proposalModule) ProposeJSON(proposalJSON string) int { + panic("not implemented") +} + +func (pm *proposalModule) ProposalsJSON(limit int, startAfter string, reverse bool) string { + panic("not implemented") +} + +func (pm *proposalModule) ProposalJSON(proposalID int) string { + panic("not implemented") +} + +func TestDAOCore(t *testing.T) { + var testValue string + handler := dao_interfaces.NewCopyMessageHandler(&testValue) + handlerFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return handler + } + + core := NewDAOCore(votingModuleFactory, []dao_interfaces.ProposalModuleFactory{proposalModuleFactory}, []dao_interfaces.MessageHandlerFactory{handlerFactory}) + if core == nil { + t.Fatal("core is nil") + } + if core.ActiveProposalModuleCount() != 1 { + t.Fatal("expected 1 active proposal module") + } + + votingMod := core.VotingModule() + if votingMod == nil { + t.Fatal("voting module is nil") + } + if votingMod.Info().Kind != "TestVoting" { + t.Fatal("voting module has wrong kind") + } + + propMods := core.ProposalModules() + if len(propMods) != 1 { + t.Fatal("expected 1 proposal module") + } + + propMod := propMods[0] + if !propMod.Enabled { + t.Fatal("proposal module is not enabled") + } + if propMod.Module == nil { + t.Fatal("proposal module is nil") + } + if propMod.Module.Info().Kind != "TestProposal" { + t.Fatal("proposal module has wrong kind") + } + + registry := core.Registry() + if registry == nil { + t.Fatal("registry is nil") + } + msg := &dao_interfaces.CopyMessage{Value: "test"} + registry.Execute(msg) + if testValue != "test" { + t.Errorf("expected testValue to be 'test', got '%s'", testValue) + } + + newProposalModule := &proposalModule{core: core} + updatePropModsMsg := &UpdateProposalModulesExecutableMessage{ + ToAdd: []dao_interfaces.IProposalModule{newProposalModule}, + ToDisable: []int{0}, + } + registry.Execute(updatePropModsMsg) + + if core.ActiveProposalModuleCount() != 1 { + t.Fatal("expected 1 active proposal module") + } + + propMods = core.ProposalModules() + if len(propMods) != 2 { + t.Fatal("expected 2 proposal modules") + } + + propMod = propMods[0] + if propMod.Enabled { + t.Errorf("old proposal module is still enabled") + } + + propMod = propMods[1] + if !propMod.Enabled { + t.Errorf("new proposal module is not enabled") + } + if propMod.Module != newProposalModule { + t.Errorf("new proposal module is not the same as the one added") + } + + newVotingModule := &votingModule{core: core} + updateVotingModMsg := &UpdateVotingModuleExecutableMessage{ + Module: newVotingModule, + } + registry.Execute(updateVotingModMsg) + + votingMod = core.VotingModule() + if votingMod != newVotingModule { + t.Errorf("new voting module is not the same as the one added") + } +} diff --git a/examples/gno.land/p/demo/daodao/core/errors.gno b/examples/gno.land/p/demo/daodao/core/errors.gno new file mode 100644 index 00000000000..a7299585a0a --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/errors.gno @@ -0,0 +1,15 @@ +package core + +import ( + "errors" +) + +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrModuleDisabledCannotExecute = errors.New("module disabled, cannot execute") + ErrNotImplemented = errors.New("not implemented") + ErrModuleAlreadyDisabled = errors.New("module already disabled") + ErrNoActiveProposalModules = errors.New("no active proposal modules") + ErrModuleAlreadyAdded = errors.New("module already added") + ErrNotSupported = errors.New("not supported") +) diff --git a/examples/gno.land/p/demo/daodao/core/expiration.gno b/examples/gno.land/p/demo/daodao/core/expiration.gno new file mode 100644 index 00000000000..a11fb0a2260 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/expiration.gno @@ -0,0 +1,47 @@ +package core + +import ( + "std" + "time" +) + +// TODO: use this pattern for Threshold and remove this file + +// Ported from https://github.com/CosmWasm/cw-utils/blob/7fce8a214f2f1e7763b8718dcbd2a6dd07f30988/src/expiration.rs + +type ( + Expiration interface { + IsExpired() bool + } + ExpirationAtHeight int64 + ExpirationAtTime time.Time + ExpirationNever struct{} +) + +func (e ExpirationAtHeight) IsExpired() bool { + return std.GetHeight() >= int64(e) +} + +func (e ExpirationAtTime) IsExpired() bool { + return time.Now().After(time.Time(e)) +} + +func (e ExpirationNever) IsExpired() bool { + return false +} + +type ( + Duration interface { + AfterCurrentBlock() Expiration + } + DurationHeight int64 + DurationTime time.Duration +) + +func (d DurationHeight) AfterCurrentBlock() Expiration { + return ExpirationAtHeight(std.GetHeight() + int64(d)) +} + +func (d DurationTime) AfterCurrentBlock() Expiration { + return ExpirationAtTime(time.Now().Add(time.Duration(d))) +} diff --git a/examples/gno.land/p/demo/daodao/core/expiration_test.gno b/examples/gno.land/p/demo/daodao/core/expiration_test.gno new file mode 100644 index 00000000000..5f0a006cc94 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/expiration_test.gno @@ -0,0 +1,15 @@ +package core + +import ( + "testing" +) + +func TestMatch(t *testing.T) { + ex := ExpirationNever{} + switch Expiration(ex).(type) { + case ExpirationNever: + t.Log("ExpirationNever") + default: + t.Fatalf("expected a match") + } +} diff --git a/examples/gno.land/p/demo/daodao/core/gno.mod b/examples/gno.land/p/demo/daodao/core/gno.mod new file mode 100644 index 00000000000..245c217ce07 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/daodao/core + +require ( + "gno.land/p/demo/daodao/interfaces" v0.0.0-latest + "gno.land/p/demo/ujson" v0.0.0-latest + "gno.land/p/demo/markdown_utils" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/core/messages.gno b/examples/gno.land/p/demo/daodao/core/messages.gno new file mode 100644 index 00000000000..14af900e0d2 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core/messages.gno @@ -0,0 +1,93 @@ +package core + +import ( + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/ujson" +) + +// UpdateProposalModules + +type UpdateProposalModulesExecutableMessage struct { + ToAdd []dao_interfaces.IProposalModule + ToDisable []int +} + +func (msg *UpdateProposalModulesExecutableMessage) Type() string { + return "gno.land/p/demo/daodao/core.UpdateProposalModules" +} + +func (msg *UpdateProposalModulesExecutableMessage) String() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateProposalModulesExecutableMessage) ToJSON() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateProposalModulesExecutableMessage) FromJSON(ast *ujson.JSONASTNode) { + panic(ErrNotImplemented) +} + +type UpdateProposalModulesMessageHandler struct { + dao dao_interfaces.IDAOCore +} + +func NewUpdateProposalModulesMessageHandler(dao dao_interfaces.IDAOCore) *UpdateProposalModulesMessageHandler { + return &UpdateProposalModulesMessageHandler{dao: dao} +} + +func (handler *UpdateProposalModulesMessageHandler) Type() string { + return UpdateProposalModulesExecutableMessage{}.Type() +} + +func (handler *UpdateProposalModulesMessageHandler) Execute(message dao_interfaces.ExecutableMessage) { + msg := message.(*UpdateProposalModulesExecutableMessage) + handler.dao.UpdateProposalModules(msg.ToAdd, msg.ToDisable) +} + +func (handler *UpdateProposalModulesMessageHandler) MessageFromJSON(ast *ujson.JSONASTNode) dao_interfaces.ExecutableMessage { + panic(ErrNotSupported) +} + +// UpdateVotingModule + +type UpdateVotingModuleExecutableMessage struct { + Module dao_interfaces.IVotingModule +} + +func (msg *UpdateVotingModuleExecutableMessage) Type() string { + return "gno.land/p/demo/daodao/core.UpdateVotingModule" +} + +func (msg *UpdateVotingModuleExecutableMessage) String() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateVotingModuleExecutableMessage) ToJSON() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateVotingModuleExecutableMessage) FromJSON(ast *ujson.JSONASTNode) { + panic(ErrNotImplemented) +} + +type UpdateVotingModuleMessageHandler struct { + dao dao_interfaces.IDAOCore +} + +func NewUpdateVotingModuleMessageHandler(dao dao_interfaces.IDAOCore) *UpdateVotingModuleMessageHandler { + return &UpdateVotingModuleMessageHandler{dao: dao} +} + +func (handler *UpdateVotingModuleMessageHandler) Type() string { + return UpdateVotingModuleExecutableMessage{}.Type() +} + +func (handler *UpdateVotingModuleMessageHandler) Execute(message dao_interfaces.ExecutableMessage) { + msg := message.(*UpdateVotingModuleExecutableMessage) + handler.dao.UpdateVotingModule(msg.Module) +} + +func (handler *UpdateVotingModuleMessageHandler) MessageFromJSON(ast *ujson.JSONASTNode) dao_interfaces.ExecutableMessage { + panic(ErrNotSupported) +} diff --git a/examples/gno.land/p/demo/daodao/interfaces/core.gno b/examples/gno.land/p/demo/daodao/interfaces/core.gno new file mode 100644 index 00000000000..eed2eda0022 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/core.gno @@ -0,0 +1,18 @@ +package dao_interfaces + +type ActivableProposalModule struct { + Enabled bool + Module IProposalModule +} + +type IDAOCore interface { + Render(path string) string + + VotingModule() IVotingModule + ProposalModules() []ActivableProposalModule + ActiveProposalModuleCount() int + Registry() *MessagesRegistry + + UpdateVotingModule(newVotingModule IVotingModule) + UpdateProposalModules(toAdd []IProposalModule, toDisable []int) +} diff --git a/examples/gno.land/p/demo/daodao/interfaces/gno.mod b/examples/gno.land/p/demo/daodao/interfaces/gno.mod new file mode 100644 index 00000000000..034cffbd413 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/daodao/interfaces + +require ( + "gno.land/p/demo/ujson" v0.0.0-latest + "gno.land/p/demo/avl" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces/messages.gno b/examples/gno.land/p/demo/daodao/interfaces/messages.gno new file mode 100644 index 00000000000..d3c50fbe942 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/messages.gno @@ -0,0 +1,26 @@ +package dao_interfaces + +import ( + "encoding/base64" + "encoding/binary" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ujson" +) + +type ExecutableMessage interface { + ujson.JSONAble + ujson.FromJSONAble + + String() string + Type() string +} + +type MessageHandler interface { + Execute(message ExecutableMessage) + MessageFromJSON(ast *ujson.JSONASTNode) ExecutableMessage + Type() string +} + +type MessageHandlerFactory func(core IDAOCore) MessageHandler diff --git a/examples/gno.land/p/demo/daodao/interfaces/messages_registry.gno b/examples/gno.land/p/demo/daodao/interfaces/messages_registry.gno new file mode 100644 index 00000000000..62ece26b3ef --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/messages_registry.gno @@ -0,0 +1,143 @@ +package dao_interfaces + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ujson" +) + +type MessagesRegistry struct { + handlers *avl.Tree +} + +func NewMessagesRegistry() *MessagesRegistry { + registry := &MessagesRegistry{handlers: avl.NewTree()} + registry.Register(NewRegisterHandlerExecutableMessageHandler(registry)) + registry.Register(NewRemoveHandlerExecutableMessageHandler(registry)) + return registry +} + +func (r *MessagesRegistry) Register(handler MessageHandler) { + r.handlers.Set(handler.Type(), handler) +} + +func (r *MessagesRegistry) Remove(t string) { + r.handlers.Remove(t) +} + +func (r *MessagesRegistry) MessagesFromJSON(messagesJSON string) []ExecutableMessage { + slice := ujson.ParseSlice(messagesJSON) + msgs := make([]ExecutableMessage, 0, len(slice)) + for _, child := range slice { + var messageType string + var payload *ujson.JSONASTNode + child.ParseObject([]*ujson.ParseKV{ + {Key: "type", Value: &messageType}, + {Key: "payload", Value: &payload}, + }) + h, ok := r.handlers.Get(messageType) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + msgs = append(msgs, h.(MessageHandler).MessageFromJSON(payload)) + } + return msgs +} + +func (r *MessagesRegistry) Execute(msg ExecutableMessage) { + h, ok := r.handlers.Get(msg.Type()) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).Execute(msg) +} + +func (r *MessagesRegistry) ExecuteMessages(msgs []ExecutableMessage) { + for _, msg := range msgs { + r.Execute(msg) + } +} + +type RegisterHandlerExecutableMessage struct { + Handler MessageHandler +} + +func (m *RegisterHandlerExecutableMessage) Type() string { + return "gno.land/p/demo/daodao/interfaces.RegisterHandler" +} + +func (m *RegisterHandlerExecutableMessage) FromJSON(ast *ujson.JSONASTNode) { + panic("not implemented") +} + +func (m *RegisterHandlerExecutableMessage) ToJSON() string { + panic("not implemented") +} + +func (m *RegisterHandlerExecutableMessage) String() string { + return m.Handler.Type() +} + +type RegisterHandlerExecutableMessageHandler struct { + registry *MessagesRegistry +} + +func NewRegisterHandlerExecutableMessageHandler(registry *MessagesRegistry) *RegisterHandlerExecutableMessageHandler { + return &RegisterHandlerExecutableMessageHandler{registry: registry} +} + +func (h *RegisterHandlerExecutableMessageHandler) Type() string { + return RegisterHandlerExecutableMessage{}.Type() +} + +func (h *RegisterHandlerExecutableMessageHandler) MessageFromJSON(ast *ujson.JSONASTNode) ExecutableMessage { + panic("not implemented") +} + +func (h *RegisterHandlerExecutableMessageHandler) Execute(msg ExecutableMessage) { + h.registry.Register(msg.(*RegisterHandlerExecutableMessage).Handler) +} + +type RemoveHandlerExecutableMessage struct { + HandlerType string +} + +func (m *RemoveHandlerExecutableMessage) Type() string { + return "gno.land/p/demo/daodao/interfaces.RemoveHandler" +} + +func (m *RemoveHandlerExecutableMessage) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseAny(&m.HandlerType) +} + +func (m *RemoveHandlerExecutableMessage) ToJSON() string { + ujson.FormatAny(m.HandlerType) +} + +func (m *RemoveHandlerExecutableMessage) String() string { + return m.HandlerType +} + +type RemoveHandlerExecutableMessageHandler struct { + registry *MessagesRegistry +} + +func NewRemoveHandlerExecutableMessageHandler(registry *MessagesRegistry) *RemoveHandlerExecutableMessageHandler { + return &RemoveHandlerExecutableMessageHandler{registry: registry} +} + +func (h *RemoveHandlerExecutableMessageHandler) Type() string { + return RemoveHandlerExecutableMessage{}.Type() +} + +func (h *RemoveHandlerExecutableMessageHandler) MessageFromJSON(ast *ujson.JSONASTNode) ExecutableMessage { + msg := &RemoveHandlerExecutableMessage{} + ast.ParseAny(msg) + return msg +} + +func (h *RemoveHandlerExecutableMessageHandler) Execute(msg ExecutableMessage) { + h.registry.Remove(msg.(*RemoveHandlerExecutableMessage).HandlerType) +} diff --git a/examples/gno.land/p/demo/daodao/interfaces/messages_registry_test.gno b/examples/gno.land/p/demo/daodao/interfaces/messages_registry_test.gno new file mode 100644 index 00000000000..e9b4306c7a7 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/messages_registry_test.gno @@ -0,0 +1,52 @@ +package dao_interfaces + +import ( + "testing" +) + +func TestRegistry(t *testing.T) { + registry := NewMessagesRegistry() + + var value string + msgHandler := NewCopyMessageHandler(&value) + + // Test register handler via message + registerMsg := &RegisterHandlerExecutableMessage{Handler: msgHandler} + registry.Execute(registerMsg) + + // Test messages execution + msgs := registry.MessagesFromJSON(`[{"type":"CopyMessage","payload":"Hello"}]`) + if len(msgs) != 1 { + t.Errorf("Expected 1 message, got %d", len(msgs)) + } + registry.Execute(msgs[0]) + if value != "Hello" { + t.Errorf("Expected value to be 'Hello', got '%s'", value) + } + + msg2 := &CopyMessage{Value: "World"} + registry.Execute(msg2) + if value != "World" { + t.Errorf("Expected value to be 'World', got '%s'", value) + } + + // Test handler removal + removeMsg := &RemoveHandlerExecutableMessage{HandlerType: msgHandler.Type()} + registry.Execute(removeMsg) + func() { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic, got none") + } + }() + registry.Execute(msg2) + }() + + // Test direct register + registry.Register(msgHandler) + msg3 := &CopyMessage{Value: "!"} + registry.Execute(msg3) + if value != "!" { + t.Errorf("Expected value to be '!', got '%s'", value) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces/messages_testing.gno b/examples/gno.land/p/demo/daodao/interfaces/messages_testing.gno new file mode 100644 index 00000000000..0a6d4ab1cdc --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/messages_testing.gno @@ -0,0 +1,54 @@ +package dao_interfaces + +import ( + "gno.land/p/demo/ujson" +) + +type CopyMessage struct { + Value string +} + +func (m *CopyMessage) Type() string { + return "CopyMessage" +} + +func (m *CopyMessage) String() string { + return m.Value +} + +func (m *CopyMessage) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseAny(&m.Value) +} + +func (m *CopyMessage) ToJSON() string { + return ujson.FormatString(m.Value) +} + +type CopyMessageHandler struct { + ptr *string +} + +func NewCopyMessageHandler(ptr *string) *CopyMessageHandler { + if ptr == nil { + panic("ptr cannot be nil") + } + return &CopyMessageHandler{ptr} +} + +func (h *CopyMessageHandler) Execute(imsg ExecutableMessage) { + msg, ok := imsg.(*CopyMessage) + if !ok { + panic("Wrong message type") + } + *h.ptr = msg.Value +} + +func (h *CopyMessageHandler) Type() string { + return "CopyMessage" +} + +func (h *CopyMessageHandler) MessageFromJSON(ast *ujson.JSONASTNode) ExecutableMessage { + var msg CopyMessage + ast.ParseAny(&msg) + return &msg +} diff --git a/examples/gno.land/p/demo/daodao/interfaces/modules.gno b/examples/gno.land/p/demo/daodao/interfaces/modules.gno new file mode 100644 index 00000000000..b1e4d4e1dba --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces/modules.gno @@ -0,0 +1,37 @@ +package dao_interfaces + +import ( + "std" + + "gno.land/p/demo/ujson" +) + +type ModuleInfo struct { + Kind string + Version string +} + +// NOTE: Some queries take a height param in DA0-DA0 contracts, but since gno seem to aim to support queries at any height, we shouldn't need it + +type IVotingModule interface { + Core() IDAOCore + Info() ModuleInfo + Render(path string) string + VotingPowerAtHeight(address std.Address, height int64) (power uint64) + TotalPowerAtHeight(height int64) uint64 +} + +type VotingModuleFactory func(core IDAOCore) IVotingModule + +type IProposalModule interface { + Core() IDAOCore + Info() ModuleInfo + Render(path string) string + Execute(proposalID int) + VoteJSON(proposalID int, voteJSON string) + ProposeJSON(proposalJSON string) int + ProposalsJSON(limit int, startAfter string, reverse bool) string + ProposalJSON(proposalID int) string +} + +type ProposalModuleFactory func(core IDAOCore) IProposalModule diff --git a/examples/gno.land/p/demo/daodao/proposal_single/dao_proposal_single.gno b/examples/gno.land/p/demo/daodao/proposal_single/dao_proposal_single.gno new file mode 100644 index 00000000000..81d1dad9ef6 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/dao_proposal_single.gno @@ -0,0 +1,379 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/ujson" +) + +type DAOProposalSingleOpts struct { + /// The threshold a proposal must reach to complete. + Threshold Threshold + /// The default maximum amount of time a proposal may be voted on + /// before expiring. + MaxVotingPeriod time.Duration + /// The minimum amount of time a proposal must be open before + /// passing. A proposal may fail before this amount of time has + /// elapsed, but it will not pass. This can be useful for + /// preventing governance attacks wherein an attacker aquires a + /// large number of tokens and forces a proposal through. + MinVotingPeriod time.Duration // 0 means no minimum + /// If set to true only members may execute passed + /// proposals. Otherwise, any address may execute a passed + /// proposal. + OnlyMembersExecute bool + /// Allows changing votes before the proposal expires. If this is + /// enabled proposals will not be able to complete early as final + /// vote information is not known until the time of proposal + /// expiration. + AllowRevoting bool + /// Information about what addresses may create proposals. + // preProposeInfo PreProposeInfo + /// If set to true proposals will be closed if their execution + /// fails. Otherwise, proposals will remain open after execution + /// failure. For example, with this enabled a proposal to send 5 + /// tokens out of a DAO's treasury with 4 tokens would be closed when + /// it is executed. With this disabled, that same proposal would + /// remain open until the DAO's treasury was large enough for it to be + /// executed. + CloseProposalOnExecutionFailure bool +} + +type DAOProposalSingle struct { + dao_interfaces.IProposalModule + + core dao_interfaces.IDAOCore + opts *DAOProposalSingleOpts + proposals []*Proposal +} + +func NewDAOProposalSingle(core dao_interfaces.IDAOCore, opts *DAOProposalSingleOpts) *DAOProposalSingle { + if core == nil { + panic("core cannot be nil") + } + + if opts == nil { + panic("opts cannot be nil") + } + + if opts.AllowRevoting { + panic("allow revoting not implemented") + } + + if opts.OnlyMembersExecute { + panic("only members execute not implemented") + } + + if opts.CloseProposalOnExecutionFailure { + panic("close proposal on execution failure not implemented") + } + + // TODO: support other threshold types + switch opts.Threshold.(type) { + case *ThresholdThresholdQuorum: + threshold := opts.Threshold.(*ThresholdThresholdQuorum) + switch threshold.Threshold.(type) { + case *PercentageThresholdMajority: + panic("not implemented") + case *PercentageThresholdPercent: + if *threshold.Threshold.(*PercentageThresholdPercent) > 10000 { + panic("opts.Threshold.Threshold must be <= 100%") + } + default: + panic("unknown Threshold type") + } + switch threshold.Quorum.(type) { + case *PercentageThresholdMajority: + panic("not implemented") + case *PercentageThresholdPercent: + if *threshold.Quorum.(*PercentageThresholdPercent) > 10000 { + panic("opts.Threshold.Quorum must be <= 100%") + } + default: + panic("unknown PercentageThreshold type") + } + default: + panic("unsupported Threshold type") + } + + return &DAOProposalSingle{core: core, opts: opts} +} + +func (d *DAOProposalSingle) Render(path string) string { + minVotingPeriodStr := "No minimum voting period" + if d.opts.MinVotingPeriod != 0 { + minVotingPeriodStr = "Min voting period: " + d.opts.MinVotingPeriod.String() + } + + executeStr := "Any address may execute passed proposals" + if d.opts.OnlyMembersExecute { + executeStr = "Only members may execute passed proposals" + } + + revotingStr := "Revoting is not allowed" + if d.opts.AllowRevoting { + revotingStr = "Revoting is allowed" + } + + closeOnExecFailureStr := "Proposals will remain open after execution failure" + if d.opts.CloseProposalOnExecutionFailure { + closeOnExecFailureStr = "Proposals will be closed if their execution fails" + } + + thresholdStr := "" + switch d.opts.Threshold.(type) { + case *ThresholdThresholdQuorum: + threshold := d.opts.Threshold.(*ThresholdThresholdQuorum) + thresholdStr = "Threshold: " + threshold.Threshold.String() + "\n\n" + + "Quorum: " + threshold.Quorum.String() + default: + panic("unsupported Threshold type") + } + + proposalsStr := "## Proposals\n" + for _, p := range d.proposals { + messagesStr := "" + for _, m := range p.Messages { + messagesStr += "- " + m.(dao_interfaces.ExecutableMessage).String() + "\n" + } + + proposalsStr += "### #" + strconv.Itoa(p.ID) + " " + p.Title + "\n" + + "Status: " + p.Status.String() + "\n\n" + + "Proposed by " + p.Proposer.String() + "\n\n" + + p.Description + "\n\n" + + "Votes summary:" + "\n\n" + + "- Yes: " + strconv.FormatUint(p.Votes.Yes, 10) + "\n" + + "- No: " + strconv.FormatUint(p.Votes.No, 10) + "\n" + + "- Abstain: " + strconv.FormatUint(p.Votes.Abstain, 10) + "\n\n" + + "Total: " + strconv.FormatUint(p.Votes.Total(), 10) + "\n" + + "#### Messages\n" + + messagesStr + + "#### Votes\n" + + p.Ballots.Iterate("", "", func(k string, v interface{}) bool { + ballot := v.(Ballot) + proposalsStr += "- " + k + " voted " + ballot.Vote.String() + "\n" + return false + }) + + proposalsStr += "\n" + } + + return "# Single choice proposals module" + "\n" + + "## Summary" + "\n" + + "Max voting period: " + d.opts.MaxVotingPeriod.String() + "\n\n" + + minVotingPeriodStr + "\n\n" + + executeStr + "\n\n" + + revotingStr + "\n\n" + + closeOnExecFailureStr + "\n\n" + + thresholdStr + "\n\n" + + proposalsStr +} + +func (d *DAOProposalSingle) Core() dao_interfaces.IDAOCore { + return d.core +} + +func (d *DAOProposalSingle) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "SingleChoice", + Version: "0.1.0", + } +} + +func (d *DAOProposalSingle) Propose(title string, description string, messages []dao_interfaces.ExecutableMessage) int { + // TODO: creation policy + + totalPower := d.core.VotingModule().TotalPowerAtHeight(0) + + id := len(d.proposals) + + prop := Proposal{ + ID: id, + Title: title, + Description: description, + Proposer: std.PrevRealm().Addr(), + StartHeight: std.GetHeight(), + // TODO: min_voting_period + // TODO: expiration + Threshold: d.opts.Threshold.Clone(), + TotalPower: totalPower, + Messages: messages, + Status: ProposalStatusOpen, + Ballots: avl.NewTree(), + AllowRevoting: d.opts.AllowRevoting, + } + prop.updateStatus() + d.proposals = append(d.proposals, &prop) + return id +} + +func (d *DAOProposalSingle) GetBallot(proposalID int, memberAddress std.Address) Ballot { + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + proposal := d.proposals[proposalID] + ballot, has := proposal.Ballots.Get(memberAddress.String()) + if !has { + panic("ballot does not exist") + } + return ballot.(Ballot) +} + +type VoteWithRationale struct { + Vote Vote + Rationale string +} + +func (v *VoteWithRationale) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseObject([]*ujson.ParseKV{ + {Key: "vote", Value: &v.Vote}, + {Key: "rationale", Value: &v.Rationale}, + }) +} + +func (d *DAOProposalSingle) VoteJSON(proposalID int, voteJSON string) { + var v VoteWithRationale + ujson.ParseAny(voteJSON, &v) + + voter := std.PrevRealm().Addr() + + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + proposal := d.proposals[proposalID] + // TODO: check proposal expiration + + votePower := d.core.VotingModule().VotingPowerAtHeight(voter, proposal.StartHeight) + if votePower == 0 { + panic("not registered") + } + + // TODO: handle revoting + if ok := proposal.Ballots.Has(voter.String()); ok { + panic("already voted") + } + proposal.Ballots.Set(voter.String(), Ballot{ + Vote: v.Vote, + Power: votePower, + Rationale: v.Rationale, + }) + + proposal.Votes.Add(v.Vote, votePower) + + proposal.updateStatus() +} + +func (d *DAOProposalSingle) Execute(proposalID int) { + executer := std.GetOrigCaller() + + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + prop := d.proposals[proposalID] + + prop.updateStatus() + if prop.Status != ProposalStatusPassed { + panic("proposal is not passed") + } + + for _, m := range prop.Messages { + d.core.Registry().Execute(m) + } + + prop.Status = ProposalStatusExecuted +} + +type ProposalRequest struct { + Title string + Description string + Messages *ujson.JSONASTNode +} + +func (pr *ProposalRequest) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseObject([]*ujson.ParseKV{ + {Key: "title", Value: &pr.Title}, + {Key: "description", Value: &pr.Description}, + {Key: "messages", Value: &pr.Messages}, + }) +} + +func (d *DAOProposalSingle) ProposeJSON(proposalJSON string) int { + var req ProposalRequest + ujson.ParseAny(proposalJSON, &req) + msgs := d.core.Registry().MessagesFromJSON(req.Messages.String()) // TODO: optimize + return d.Propose(req.Title, req.Description, msgs) +} + +func (d *DAOProposalSingle) Proposals() []*Proposal { + return d.proposals +} + +func (d *DAOProposalSingle) ProposalsJSON(limit int, startAfter string, reverse bool) string { + return ujson.FormatSlice(d.proposals) +} + +func (d *DAOProposalSingle) ProposalJSON(proposalID int) string { + if proposalID < 0 || proposalID >= len(d.proposals) { + panic("proposal does not exist") + } + return ujson.FormatAny(d.proposals[proposalID]) +} + +func (d *DAOProposalSingle) Threshold() Threshold { + return d.opts.Threshold +} + +func (proposal *Proposal) updateStatus() { + if proposal.Status == ProposalStatusOpen && proposal.isPassed() { + proposal.Status = ProposalStatusPassed + return + } +} + +func (proposal *Proposal) isPassed() bool { + switch proposal.Threshold.(type) { + case *ThresholdAbsolutePercentage: + panic("ThresholdAbsolutePercentage not implemented") + case *ThresholdThresholdQuorum: + thresholdObj := proposal.Threshold.(*ThresholdThresholdQuorum) + + threshold := thresholdObj.Threshold + quorum := thresholdObj.Quorum + + totalPower := proposal.TotalPower + + if !doesVoteCountPass(proposal.Votes.Total(), totalPower, quorum) { + return false + } + + // TODO: handle expiration + options := totalPower - proposal.Votes.Abstain + return doesVoteCountPass(proposal.Votes.Yes, options, threshold) + case *ThresholdAbsoluteCount: + panic("ThresholdAbsoluteCount not implemented") + default: + panic("unknown Threshold type") + } +} + +func doesVoteCountPass(yesVotes uint64, options uint64, percent PercentageThreshold) bool { + switch percent.(type) { + case *PercentageThresholdMajority: + panic("not implemented") + case *PercentageThresholdPercent: + if options == 0 { + return false + } + percentValue := uint64(*percent.(*PercentageThresholdPercent)) + votes := yesVotes * 10000 + threshold := options * percentValue + return votes >= threshold + default: + panic("unknown PercentageThreshold type") + } +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single/gno.mod b/examples/gno.land/p/demo/daodao/proposal_single/gno.mod new file mode 100644 index 00000000000..a064d1595fa --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/daodao/proposal_single + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/daodao/interfaces" v0.0.0-latest + "gno.land/p/demo/ujson" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/proposal_single/proposal_test.gno b/examples/gno.land/p/demo/daodao/proposal_single/proposal_test.gno new file mode 100644 index 00000000000..4cca0efe63d --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/proposal_test.gno @@ -0,0 +1,64 @@ +package dao_proposal_single + +import ( + "testing" + + "gno.land/p/demo/avl" + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/ujson" +) + +type NoopMessage struct{} + +var _ dao_interfaces.ExecutableMessage = (*NoopMessage)(nil) + +func (m NoopMessage) String() string { + return "noop" +} + +func (m NoopMessage) Type() string { + return "noop-type" +} + +func (m NoopMessage) ToJSON() string { + return ujson.FormatString(m.String()) +} + +func (m NoopMessage) FromJSON(ast *ujson.JSONASTNode) { + var val string + ast.ParseAny(&val) + if val != m.String() { + panic("invalid noop message") + } +} + +func TestProposalJSON(t *testing.T) { + props := []Proposal{ + { + ID: 0, + Title: "Prop #0", + Description: "Wolol0\n\t\r", + Proposer: "0x1234567890", + Votes: Votes{ + Yes: 7, + No: 21, + Abstain: 42, + }, + Ballots: avl.NewTree(), + }, + { + ID: 1, + Title: "Prop #1", + Description: `Wolol1\"`, + Proposer: "0x1234567890", + Status: ProposalStatusExecuted, + Messages: []dao_interfaces.ExecutableMessage{NoopMessage{}, NoopMessage{}, NoopMessage{}}, + }, + } + props[0].Ballots.Set("0x1234567890", Ballot{Power: 1, Vote: VoteYes, Rationale: "test"}) + str := ujson.FormatSlice(props) + expected := `[{"id":0,"title":"Prop #0","description":"Wolol0\n\t\r","proposer":"0x1234567890","startHeight":0,"threshold":null,"totalPower":0,"messages":[],"status":"Open","votes":{"yes":7,"no":21,"abstain":42},"allowRevoting":false,"ballots":{"0x1234567890":{"power":1,"vote":"Yes","rationale":"test"}}},{"id":1,"title":"Prop #1","description":"Wolol1\\\"","proposer":"0x1234567890","startHeight":0,"threshold":null,"totalPower":0,"messages":[{"type":"noop-type","payload":"noop"},{"type":"noop-type","payload":"noop"},{"type":"noop-type","payload":"noop"}],"status":"Executed","votes":{"yes":0,"no":0,"abstain":0},"allowRevoting":false,"ballots":{}}]` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single/threshold.gno b/examples/gno.land/p/demo/daodao/proposal_single/threshold.gno new file mode 100644 index 00000000000..b911ab7c2f7 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/threshold.gno @@ -0,0 +1,151 @@ +package dao_proposal_single + +import ( + "strconv" + + "gno.land/p/demo/ujson" +) + +// ported from https://github.com/DA0-DA0/dao-contracts/blob/7776858e780f1ce9f038a3b06cce341dd41d2189/packages/dao-voting/src/threshold.rs + +type PercentageThreshold interface { + String() string + Clone() PercentageThreshold + ToJSON() string +} + +func parseUnion(ast *ujson.JSONASTNode, kv []*ujson.ParseKV) interface{} { + if ast.Kind != ujson.JSONKindObject { + panic("union is not an object") + } + if len(ast.ObjectChildren) != 1 { + panic("union object does not have exactly one field") + } + k, node := ast.ObjectChildren[0].Key, ast.ObjectChildren[0].Value + for _, kv := range kv { + if kv.Key == k { + node.ParseAny(kv.Value) + return kv.Value + } + } + panic("unknown union type") // TODO: expected one of ... +} + +func formatUnionMember(name string, val interface{}, raw bool) string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: name, Value: val, Raw: raw}, + }) +} + +func PercentageThresholdFromJSON(ast *ujson.JSONASTNode) PercentageThreshold { + p := PercentageThresholdPercent(0) + return parseUnion(ast, []*ujson.ParseKV{ + {"majority", &PercentageThresholdMajority{}}, + {"percent", &p}, + }).(PercentageThreshold) +} + +type PercentageThresholdMajority struct{} + +func (p *PercentageThresholdMajority) String() string { + return "Majority" +} + +func (p *PercentageThresholdMajority) Clone() PercentageThreshold { + return &PercentageThresholdMajority{} +} + +func (p *PercentageThresholdMajority) ToJSON() string { + return formatUnionMember("majority", "{}", true) +} + +type PercentageThresholdPercent uint16 // 4 decimals fixed point + +func (p *PercentageThresholdPercent) String() string { + s := strconv.FormatUint(uint64(*p)/100, 10) + decPart := uint64(*p) % 100 + if decPart != 0 { + s += "." + strconv.FormatUint(decPart, 10) + } + return s + "%" +} + +func (p *PercentageThresholdPercent) FromJSON(ast *ujson.JSONASTNode) { + var val uint32 + ujson.ParseAny(ast.Value, &val) + *p = PercentageThresholdPercent(val) +} + +func (p *PercentageThresholdPercent) Clone() PercentageThreshold { + c := *p + return &c +} + +func (p *PercentageThresholdPercent) ToJSON() string { + return formatUnionMember("percent", uint64(*p), false) +} + +type Threshold interface { + Clone() Threshold + ToJSON() string +} + +func ThresholdFromJSON(ast *ujson.JSONASTNode) Threshold { + ac := ThresholdAbsoluteCount(0) + return parseUnion(ast, []*ujson.ParseKV{ + // TODO: {Key: "absolutePercentage"}, + {Key: "thresholdQuorum", Value: &ThresholdThresholdQuorum{}}, + {Key: "absoluteCount", Value: &ac}, + }).(Threshold) +} + +type ThresholdAbsolutePercentage PercentageThreshold + +func (t *ThresholdAbsolutePercentage) Clone() Threshold { + c := (*t).(PercentageThreshold).Clone() + return (*ThresholdAbsolutePercentage)(c) +} + +func (t *ThresholdAbsolutePercentage) ToJSON() string { + return formatUnionMember("absolutePercentage", (t).(*PercentageThreshold), false) +} + +type ThresholdThresholdQuorum struct { + Threshold PercentageThreshold + Quorum PercentageThreshold +} + +func (t *ThresholdThresholdQuorum) Clone() Threshold { + return &ThresholdThresholdQuorum{ + Threshold: t.Threshold.Clone(), + Quorum: t.Quorum.Clone(), + } +} + +func (t *ThresholdThresholdQuorum) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseObject([]*ujson.ParseKV{ + {Key: "threshold", CustomParser: func(ast *ujson.JSONASTNode) { + t.Threshold = PercentageThresholdFromJSON(ast) + }}, + {Key: "quorum", CustomParser: func(ast *ujson.JSONASTNode) { + t.Quorum = PercentageThresholdFromJSON(ast) + }}, + }) +} + +func (t *ThresholdThresholdQuorum) ToJSON() string { + return formatUnionMember("thresholdQuorum", ujson.FormatObject([]ujson.FormatKV{ + {Key: "threshold", Value: t.Threshold}, + {Key: "quorum", Value: t.Quorum}, + }), true) +} + +type ThresholdAbsoluteCount uint64 + +func (t *ThresholdAbsoluteCount) Clone() Threshold { + return &ThresholdAbsoluteCount(*t) +} + +func (t *ThresholdAbsoluteCount) ToJSON() string { + return formatUnionMember("absoluteCount", uint64(*t), false) +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single/types.gno b/examples/gno.land/p/demo/daodao/proposal_single/types.gno new file mode 100644 index 00000000000..b8460b89fce --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/types.gno @@ -0,0 +1,188 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/ujson" +) + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +func (b Ballot) ToJSON() string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: "power", Value: b.Power}, + {Key: "vote", Value: b.Vote}, + {Key: "rationale", Value: b.Rationale}, + }) +} + +type Votes struct { + Yes uint64 + No uint64 + Abstain uint64 +} + +func (v *Votes) Add(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes += power + case VoteNo: + v.No += power + case VoteAbstain: + v.Abstain += power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Remove(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes -= power + case VoteNo: + v.No -= power + case VoteAbstain: + v.Abstain -= power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Total() uint64 { + return v.Yes + v.No + v.Abstain +} + +func (v Votes) ToJSON() string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: "yes", Value: v.Yes}, + {Key: "no", Value: v.No}, + {Key: "abstain", Value: v.Abstain}, + }) +} + +type Proposal struct { + ID int + Title string + Description string + Proposer std.Address + StartHeight int64 + // TODO: min_voting_period + // TODO: expiration + Threshold Threshold + TotalPower uint64 + Messages []dao_interfaces.ExecutableMessage + Status ProposalStatus + Votes Votes + AllowRevoting bool + + // not in DA0-DA0 implementation: + + Ballots *avl.Tree +} + +var _ ujson.JSONAble = (*Proposal)(nil) + +type messageWithType struct { + Type string + Message dao_interfaces.ExecutableMessage +} + +func (m *messageWithType) ToJSON() string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: "type", Value: m.Type}, + {Key: "payload", Value: m.Message}, + }) +} + +func formatMessages(messages []dao_interfaces.ExecutableMessage) string { + var out []*messageWithType + for _, m := range messages { + out = append(out, &messageWithType{ + Type: m.Type(), + Message: m, + }) + } + return ujson.FormatSlice(out) +} + +func (p Proposal) ToJSON() string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: "id", Value: p.ID}, + {Key: "title", Value: p.Title}, + {Key: "description", Value: p.Description}, + {Key: "proposer", Value: p.Proposer}, + {Key: "startHeight", Value: p.StartHeight}, + {Key: "threshold", Value: p.Threshold}, + {Key: "totalPower", Value: p.TotalPower}, + {Key: "messages", Value: formatMessages(p.Messages), Raw: true}, + {Key: "status", Value: p.Status}, + {Key: "votes", Value: p.Votes}, + {Key: "allowRevoting", Value: p.AllowRevoting}, + + {Key: "ballots", Value: p.Ballots}, + }) +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +func (p ProposalStatus) ToJSON() string { + return ujson.FormatString(p.String()) +} + +func (p ProposalStatus) String() string { + switch p { + case ProposalStatusOpen: + return "Open" + case ProposalStatusPassed: + return "Passed" + case ProposalStatusExecuted: + return "Executed" + default: + return "Unknown(" + strconv.Itoa(int(p)) + ")" + } +} + +type Vote int + +const ( + VoteYes Vote = iota + VoteNo + VoteAbstain +) + +func (v Vote) ToJSON() string { + return ujson.FormatString(v.String()) +} + +func (v *Vote) FromJSON(ast *ujson.JSONASTNode) { + var val int + ast.ParseAny(&val) + // FIXME: validate + *v = Vote(val) +} + +func (v Vote) String() string { + switch v { + case VoteYes: + return "Yes" + case VoteNo: + return "No" + case VoteAbstain: + return "Abstain" + default: + return "Unknown(" + strconv.Itoa(int(v)) + ")" + } +} diff --git a/examples/gno.land/p/demo/daodao/proposal_single/update_settings.gno b/examples/gno.land/p/demo/daodao/proposal_single/update_settings.gno new file mode 100644 index 00000000000..b6236d18f2b --- /dev/null +++ b/examples/gno.land/p/demo/daodao/proposal_single/update_settings.gno @@ -0,0 +1,80 @@ +package dao_proposal_single + +import ( + "encoding/binary" + "std" + "strings" + + "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/ujson" +) + +// TODO: convert to json + +type UpdateSettingsMessage struct { + dao_interfaces.ExecutableMessage + + Threshold Threshold +} + +func (usm *UpdateSettingsMessage) Type() string { + return "gno.land/p/demo/daodao/proposal_single.UpdateSettings" +} + +func (usm *UpdateSettingsMessage) String() string { + ss := []string{usm.Type()} + switch usm.Threshold.(type) { + case *ThresholdThresholdQuorum: + ss = append(ss, "Threshold type: ThresholdQuorum\n\nThreshold: "+usm.Threshold.(*ThresholdThresholdQuorum).Threshold.String()+"\n\nQuorum: "+usm.Threshold.(*ThresholdThresholdQuorum).Quorum.String()) + default: + ss = append(ss, "Threshold type: unknown") + } + return strings.Join(ss, "\n--\n") +} + +func (usm *UpdateSettingsMessage) ToJSON() string { + return ujson.FormatObject([]ujson.FormatKV{ + {Key: "threshold", Value: usm.Threshold}, + }) +} + +func (usm *UpdateSettingsMessage) FromJSON(ast *ujson.JSONASTNode) { + ast.ParseObject([]*ujson.ParseKV{ + {Key: "threshold", CustomParser: func(node *ujson.JSONASTNode) { + usm.Threshold = ThresholdFromJSON(node) + }}, + }) +} + +func NewUpdateSettingsHandler(mod *DAOProposalSingle) dao_interfaces.MessageHandler { + return &updateSettingsHandler{mod: mod} +} + +type updateSettingsHandler struct { + dao_interfaces.MessageHandler + + mod *DAOProposalSingle +} + +func (h *updateSettingsHandler) Execute(message dao_interfaces.ExecutableMessage) { + usm := message.(*UpdateSettingsMessage) + + switch usm.Threshold.(type) { + case *ThresholdThresholdQuorum: + // FIXME: validate better + h.mod.opts.Threshold = usm.Threshold.(*ThresholdThresholdQuorum) + return + default: + panic("unsupported threshold type") + } +} + +func (h *updateSettingsHandler) Type() string { + return UpdateSettingsMessage{}.Type() +} + +func (h *updateSettingsHandler) MessageFromJSON(ast *ujson.JSONASTNode) dao_interfaces.ExecutableMessage { + var usm UpdateSettingsMessage + ast.ParseAny(&usm) + return &usm +} diff --git a/examples/gno.land/p/demo/daodao/voting_group/gno.mod b/examples/gno.land/p/demo/daodao/voting_group/gno.mod new file mode 100644 index 00000000000..c3525f6b849 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/daodao/voting_group + +require ( + "gno.land/p/demo/daodao/interfaces" v0.0.0-latest + "gno.land/p/demo/markdown_utils" v0.0.0-latest + "gno.land/r/demo/groups" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/voting_group/voting_group.gno b/examples/gno.land/p/demo/daodao/voting_group/voting_group.gno new file mode 100644 index 00000000000..5b8050b0155 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group/voting_group.gno @@ -0,0 +1,42 @@ +package dao_voting_group + +import ( + "std" + + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/p/demo/markdown_utils" + "gno.land/r/demo/groups" +) + +type VotingGroup struct { + dao_interfaces.IVotingModule + + groupID groups.GroupID +} + +func NewVotingGroup(groupID groups.GroupID) dao_interfaces.IVotingModule { + return &VotingGroup{groupID: groupID} +} + +func (v *VotingGroup) VotingPowerAtHeight(addr std.Address, height int64) uint64 { + return uint64(groups.GetMemberWeightByAddress(v.groupID, addr, height)) +} + +func (v *VotingGroup) TotalPowerAtHeight(height int64) uint64 { + return uint64(groups.GetGroupTotalWeight(v.groupID, height)) +} + +func (v *VotingGroup) Render(path string) string { + s := "# Group Voting Module\n" + if groupName, found := groups.GetGroupNameFromID(v.groupID); found { + s = "# [Group](/r/demo/groups:" + groupName + ") Voting Module\n" + s += markdown_utils.Indent(groups.Render(groupName)) + } else { + s += "Group not found" + } + return s +} + +func (v *VotingGroup) GetGroupID() groups.GroupID { + return v.groupID +} diff --git a/examples/gno.land/p/demo/daodao/voting_group/voting_group_test.gno b/examples/gno.land/p/demo/daodao/voting_group/voting_group_test.gno new file mode 100644 index 00000000000..910cf77a268 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/voting_group/voting_group_test.gno @@ -0,0 +1,21 @@ +package dao_voting_group + +import ( + "std" + "testing" + + dao_interfaces "gno.land/p/demo/daodao/interfaces" + "gno.land/r/demo/groups" +) + +func TestVotingGroup(t *testing.T) { + g := groups.CreateGroup("test_voting_group") + v := NewVotingGroup(g) + var i dao_interfaces.IVotingModule + i = v + got := i.TotalPowerAtHeight(0) + expected := uint64(0) + if got != expected { + t.Fatalf("expected %q, got %q.", expected, got) + } +} diff --git a/examples/gno.land/p/demo/flags_index/flags_index.gno b/examples/gno.land/p/demo/flags_index/flags_index.gno new file mode 100644 index 00000000000..cdf6e6b92b5 --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/flags_index.gno @@ -0,0 +1,162 @@ +package flags_index + +import ( + "strconv" + + "gno.land/p/demo/avl" +) + +type FlagID string + +type FlagCount struct { + FlagID FlagID + Count uint64 +} + +type FlagsIndex struct { + flagsCounts []*FlagCount // sorted by count descending; TODO: optimize using big brain datastructure + flagsCountsByID *avl.Tree // key: flagID -> FlagCount + flagsByFlaggerID *avl.Tree // key: flaggerID -> *avl.Tree key: flagID -> struct{} +} + +func NewFlagsIndex() *FlagsIndex { + return &FlagsIndex{ + flagsCountsByID: avl.NewTree(), + flagsByFlaggerID: avl.NewTree(), + } +} + +func (fi *FlagsIndex) HasFlagged(flagID FlagID, flaggerID string) bool { + if flagsByFlagID, ok := fi.flagsByFlaggerID.Get(flaggerID); ok { + if flagsByFlagID.(*avl.Tree).Has(string(flagID)) { + return true + } + } + return false +} + +func (fi *FlagsIndex) GetFlagCount(flagID FlagID) uint64 { + if flagCount, ok := fi.flagsCountsByID.Get(string(flagID)); ok { + return flagCount.(*FlagCount).Count + } + return 0 +} + +func (fi *FlagsIndex) GetFlags(limit uint64, offset uint64) []*FlagCount { + if limit == 0 { + return nil + } + if offset >= uint64(len(fi.flagsCounts)) { + return nil + } + if offset+limit > uint64(len(fi.flagsCounts)) { + limit = uint64(len(fi.flagsCounts)) - offset + } + return fi.flagsCounts[offset : offset+limit] +} + +func (fi *FlagsIndex) Flag(flagID FlagID, flaggerID string) { + // update flagsByFlaggerID + var flagsByFlagID *avl.Tree + if existingFlagsByFlagID, ok := fi.flagsByFlaggerID.Get(flaggerID); ok { + flagsByFlagID = existingFlagsByFlagID.(*avl.Tree) + if flagsByFlagID.(*avl.Tree).Has(string(flagID)) { + panic("already flagged") + } + } else { + newFlagsByFlagID := avl.NewTree() + fi.flagsByFlaggerID.Set(flaggerID, newFlagsByFlagID) + flagsByFlagID = newFlagsByFlagID + } + flagsByFlagID.Set(string(flagID), struct{}{}) + + // update flagsCountsByID and flagsCounts + iFlagCount, ok := fi.flagsCountsByID.Get(string(flagID)) + if !ok { + flagCount := &FlagCount{FlagID: flagID, Count: 1} + fi.flagsCountsByID.Set(string(flagID), flagCount) + fi.flagsCounts = append(fi.flagsCounts, flagCount) // this is valid because 1 will always be the lowest count and we want the newest flags to be last + } else { + flagCount := iFlagCount.(*FlagCount) + flagCount.Count++ + // move flagCount to correct position in flagsCounts + for i := len(fi.flagsCounts) - 1; i > 0; i-- { + if fi.flagsCounts[i].Count > fi.flagsCounts[i-1].Count { + fi.flagsCounts[i], fi.flagsCounts[i-1] = fi.flagsCounts[i-1], fi.flagsCounts[i] + } else { + break + } + } + } +} + +func (fi *FlagsIndex) ClearFlagCount(flagID FlagID) { + // find flagCount in byID + if !fi.flagsCountsByID.Has(string(flagID)) { + return + } + + // remove from byID + fi.flagsCountsByID.Remove(string(flagID)) + + // remove from byCount, we need to recreate the slice since splicing is broken + newByCount := []*FlagCount{} + for i := range fi.flagsCounts { + if fi.flagsCounts[i].FlagID == flagID { + continue + } + newByCount = append(newByCount, fi.flagsCounts[i]) + } + fi.flagsCounts = newByCount + + // update flagsByFlaggerID + var empty []string + fi.flagsByFlaggerID.Iterate("", "", func(key string, value interface{}) bool { + t := value.(*avl.Tree) + t.Remove(string(flagID)) + if t.Size() == 0 { + empty = append(empty, key) + } + return false + }) + for _, key := range empty { + fi.flagsByFlaggerID.Remove(key) + } +} + +func (fi *FlagsIndex) Dump() string { + str := "" + + str += "## flagsCounts:\n" + for i := range fi.flagsCounts { + str += "- " + if fi.flagsCounts[i] == nil { + str += "nil (" + strconv.Itoa(i) + ")\n" + continue + } + str += string(fi.flagsCounts[i].FlagID) + " " + strconv.FormatUint(fi.flagsCounts[i].Count, 10) + "\n" + } + + str += "\n## flagsCountsByID:\n" + fi.flagsCountsByID.Iterate("", "", func(key string, value interface{}) bool { + str += "- " + if value == nil { + str += "nil (" + key + ")\n" + return false + } + str += key + ": " + string(value.(*FlagCount).FlagID) + " " + strconv.FormatUint(value.(*FlagCount).Count, 10) + "\n" + return false + }) + + str += "\n## flagsByFlaggerID:\n" + fi.flagsByFlaggerID.Iterate("", "", func(key string, value interface{}) bool { + str += "- " + key + ":\n" + value.(*avl.Tree).Iterate("", "", func(key string, value interface{}) bool { + str += " - " + key + "\n" + return false + }) + return false + }) + + return str +} diff --git a/examples/gno.land/p/demo/flags_index/gno.mod b/examples/gno.land/p/demo/flags_index/gno.mod new file mode 100644 index 00000000000..3da6281f480 --- /dev/null +++ b/examples/gno.land/p/demo/flags_index/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/flags_index + +require ( + "gno.land/p/demo/avl" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/havl/gno.mod b/examples/gno.land/p/demo/havl/gno.mod new file mode 100644 index 00000000000..bfec8aa60fd --- /dev/null +++ b/examples/gno.land/p/demo/havl/gno.mod @@ -0,0 +1,5 @@ +module gno.land/p/demo/havl + +require ( + "gno.land/p/demo/avl" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/havl/havl.gno b/examples/gno.land/p/demo/havl/havl.gno new file mode 100644 index 00000000000..c45dea30b04 --- /dev/null +++ b/examples/gno.land/p/demo/havl/havl.gno @@ -0,0 +1,128 @@ +package havl + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" +) + +type Tree struct { + root avl.Tree // height -> *avl.Tree + initialHeight int64 +} + +// FIXME: don't cast height to int + +// this is not optimized at all, we make a full copy on write + +func NewTree() *Tree { + return &Tree{initialHeight: std.GetHeight()} +} + +func (t *Tree) Size(height int64) int { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Size() +} + +func (t *Tree) Has(key string, height int64) (has bool) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Has(key) +} + +func (t *Tree) Get(key string, height int64) (value interface{}, exists bool) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Get(key) +} + +func (t *Tree) GetByIndex(index int, height int64) (key string, value interface{}) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.GetByIndex(index) +} + +func (t *Tree) Set(key string, value interface{}) (updated bool) { + root := t.getOrCreateCurrentRoot() + return root.Set(key, value) +} + +func (t *Tree) Remove(key string) (value interface{}, removed bool) { + root := t.getOrCreateCurrentRoot() + return root.Remove(key) +} + +// Shortcut for TraverseInRange. +func (t *Tree) Iterate(start, end string, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Iterate(start, end, cb) +} + +// Shortcut for TraverseInRange. +func (t *Tree) ReverseIterate(start, end string, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.ReverseIterate(start, end, cb) +} + +// Shortcut for TraverseByOffset. +func (t *Tree) IterateByOffset(offset int, count int, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.IterateByOffset(offset, count, cb) +} + +// Shortcut for TraverseByOffset. +func (t *Tree) ReverseIterateByOffset(offset int, count int, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.ReverseIterateByOffset(offset, count, cb) +} + +func (t *Tree) GetSnapshot(height int64) (*avl.Tree, int64) { + key := getPaddedKey(height) + var snapshot *avl.Tree + snapshotHeight := int(t.initialHeight) + t.root.ReverseIterate("", key, func(key string, value interface{}) bool { + snapshot = value.(*avl.Tree) + var err error + snapshotHeight, err = strconv.Atoi(key) + if err != nil { + panic("internal error: failed to unmarshal key") + } + return true + }) + if snapshot == nil { + snapshot = avl.NewTree() + } + return snapshot, int64(snapshotHeight) +} + +// utils + +func getPaddedKey(height int64) string { + if height <= 0 { + height = std.GetHeight() + } + val := strconv.Itoa(int(height)) + return strings.Repeat("0", len("9223372036854775807")-len(val)) + val +} + +func clone(t *avl.Tree) *avl.Tree { + r := avl.NewTree() + t.Iterate("", "", func(key string, value interface{}) bool { + r.Set(key, value) + return false + }) + return r +} + +func (t *Tree) getOrCreateCurrentRoot() *avl.Tree { + key := getPaddedKey(0) + iroot, ok := t.root.Get(key) + var root *avl.Tree + if ok { + root = iroot.(*avl.Tree) + } else { + snapshot, _ := t.GetSnapshot(0) + root = clone(snapshot) + t.root.Set(key, root) + } + return root +} diff --git a/examples/gno.land/p/demo/markdown_utils/gno.mod b/examples/gno.land/p/demo/markdown_utils/gno.mod new file mode 100644 index 00000000000..77c4f2f271f --- /dev/null +++ b/examples/gno.land/p/demo/markdown_utils/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/markdown_utils \ No newline at end of file diff --git a/examples/gno.land/p/demo/markdown_utils/markdown_utils.gno b/examples/gno.land/p/demo/markdown_utils/markdown_utils.gno new file mode 100644 index 00000000000..c6d18af8770 --- /dev/null +++ b/examples/gno.land/p/demo/markdown_utils/markdown_utils.gno @@ -0,0 +1,26 @@ +package markdown_utils + +import ( + "strings" +) + +// this function take as input a markdown string and add an indentation level to markdown titles +func Indent(markdown string) string { + // split the markdown string into lines + lines := strings.Split(markdown, "\n") + + // iterate over the lines + for i, line := range lines { + // if the line starts with a markdown title + if strings.HasPrefix(line, "#") { + // add an indentation level to the title + lines[i] = "#" + line + } + } + + // join the lines back into a string + return strings.Join(lines, "\n") +} + +// thanks copilot this is perfect xD +// I just renamed it, AddIndentationLevelToMarkdownTitles was too long diff --git a/examples/gno.land/p/demo/ujson/format.gno b/examples/gno.land/p/demo/ujson/format.gno new file mode 100644 index 00000000000..75c97f30ad9 --- /dev/null +++ b/examples/gno.land/p/demo/ujson/format.gno @@ -0,0 +1,137 @@ +package ujson + +// This package strives to have the same behavior as json.Marshal but does not support all types and returns strings + +import ( + "errors" + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl" + "gno.land/r/demo/users" +) + +type JSONAble interface { + ToJSON() string +} + +type FormatKV struct { + Key string + Value interface{} + Raw bool +} + +// does not work for slices, use FormatSlice instead +func FormatAny(p interface{}) string { + switch p.(type) { + case std.Address: + return FormatString(string(p.(std.Address))) + case *avl.Tree: + return FormatAVLTree(p.(*avl.Tree)) + case avl.Tree: + return FormatAVLTree(&p.(avl.Tree)) + case JSONAble: + return p.(JSONAble).ToJSON() + case string: + return FormatString(p.(string)) + case uint64: + return FormatUint64(p.(uint64)) + case uint32: + return FormatUint64(uint64(p.(uint32))) + case uint: + return FormatUint64(uint64(p.(uint))) + case int64: + return FormatInt64(p.(int64)) + case int32: + return FormatInt64(int64(p.(int32))) + case int: + return FormatInt64(int64(p.(int))) + case float32: + panic("float32 not implemented") + case float64: + panic("float64 not implemented") + case bool: + return FormatBool(p.(bool)) + case time.Time: + return FormatTime(p.(time.Time)) + case users.AddressOrName: + return FormatString(string(p.(users.AddressOrName))) + default: + return "null" + } +} + +// loosely ported from https://cs.opensource.google/go/go/+/master:src/time/time.go;l=1357?q=appendStrictRFC3339&ss=go%2Fgo +func FormatTime(t time.Time) string { + s := t.Format(time.RFC3339Nano) + b := []byte(s) + + // Not all valid Go timestamps can be serialized as valid RFC 3339. + // Explicitly check for these edge cases. + // See https://go.dev/issue/4556 and https://go.dev/issue/54580. + n0 := 0 + num2 := func(b []byte) byte { return 10*(b[0]-'0') + (b[1] - '0') } + switch { + case b[n0+len("9999")] != '-': // year must be exactly 4 digits wide + panic(errors.New("year outside of range [0,9999]")) + case b[len(b)-1] != 'Z': + c := b[len(b)-len("Z07:00")] + if ('0' <= c && c <= '9') || num2(b[len(b)-len("07:00"):]) >= 24 { + panic(errors.New("timezone hour outside of range [0,23]")) + } + } + return FormatString(string(b)) +} + +func FormatUint64(i uint64) string { + return strconv.FormatUint(i, 10) +} + +func FormatInt64(i int64) string { + return strconv.FormatInt(i, 10) +} + +func FormatSlice(s []interface{}) string { + elems := make([]string, len(s)) + for i, elem := range s { + elems[i] = FormatAny(elem) + } + return "[" + strings.Join(elems, ",") + "]" +} + +func FormatObject(kv []FormatKV) string { + elems := make([]string, len(kv)) + i := 0 + for _, elem := range kv { + var val string + if elem.Raw { + val = elem.Value.(string) + } else { + val = FormatAny(elem.Value) + } + elems[i] = FormatString(elem.Key) + ":" + val + i++ + } + return "{" + strings.Join(elems, ",") + "}" +} + +func FormatBool(b bool) string { + if b { + return "true" + } + return "false" +} + +func FormatAVLTree(t *avl.Tree) string { + if t == nil { + return "{}" + } + kv := make([]FormatKV, 0, t.Size()) + t.Iterate("", "", func(key string, value interface{}) bool { + kv = append(kv, FormatKV{key, value, false}) + return false + }) + return FormatObject(kv) +} diff --git a/examples/gno.land/p/demo/ujson/gno.mod b/examples/gno.land/p/demo/ujson/gno.mod new file mode 100644 index 00000000000..140d95976db --- /dev/null +++ b/examples/gno.land/p/demo/ujson/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/ujson + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/r/demo/users" v0.0.0-latest + "gno.land/p/demo/utf16" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/ujson/parse.gno b/examples/gno.land/p/demo/ujson/parse.gno new file mode 100644 index 00000000000..e004d30fdaa --- /dev/null +++ b/examples/gno.land/p/demo/ujson/parse.gno @@ -0,0 +1,589 @@ +package ujson + +import ( + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl" + "gno.land/r/demo/users" +) + +// https://stackoverflow.com/a/4150626 +const whitespaces = " \t\n\r" + +type FromJSONAble interface { + FromJSON(ast *JSONASTNode) +} + +// does not work for slices, use ast exploration instead +func (ast *JSONASTNode) ParseAny(ptr *interface{}) { + switch ptr.(type) { + case *std.Address: + *ptr.(*std.Address) = std.Address(ParseString(ast.Value)) + case **avl.Tree: + panic("*avl.Tree not implemented, there is no way to know the type of the tree values, use ParseAVLTree instead") + case *avl.Tree: + panic("avl.Tree not implemented, there is no way to know the type of the tree values, use ParseAVLTree instead") + case *string: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindString { + panic("not a string") + } + *ptr.(*string) = ParseString(ast.Value) + case *uint64: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint64) = ParseUint64(ast.Value) + case *uint32: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint32) = uint32(ParseUint64(ast.Value)) + case *uint: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint) = uint(ParseUint64(ast.Value)) + case *int64: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int64) = ParseInt64(ast.Value) + case *int32: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int32) = int32(ParseInt64(ast.Value)) + case *int: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int) = int(ParseInt64(ast.Value)) + case *float64: + panic("float64 not implemented") + case *float32: + panic("float32 not implemented") + case *bool: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindTrue && ast.ValueKind != JSONTokenKindFalse { + panic("not a bool") + } + *ptr.(*bool) = ast.ValueKind == JSONTokenKindTrue + case *FromJSONAble: + (*(ptr.(*FromJSONAble))).FromJSON(ast) + case FromJSONAble: + ptr.(FromJSONAble).FromJSON(ast) + case **JSONASTNode: + *ptr.(**JSONASTNode) = ast + case *time.Time: + ast.ParseTime(ptr.(*time.Time)) + case *users.AddressOrName: + s := ParseString(ast.Value) + *ptr.(*users.AddressOrName) = users.AddressOrName(s) + default: + if ast.Kind == JSONKindValue && ast.ValueKind == JSONTokenKindNull { + *ptr = nil + return + } + panic("type not defined for `" + ast.String() + "`") + } +} + +// loosely ported from https://cs.opensource.google/go/go/+/master:src/time/time.go;l=1370?q=appendStrictRFC3339&ss=go%2Fgo +// it's not a full port since it would require copying lot of utils +func (ast *JSONASTNode) ParseTime(t *time.Time) { + if ast.Kind != JSONKindValue && ast.ValueKind != JSONTokenKindString { + panic("time is not a string") + } + s := ParseString(ast.Value) + var err error + *t, err = time.Parse(time.RFC3339Nano, s) + if err != nil { + panic(err) + } +} + +func ParseUint64(s string) uint64 { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return uint64(val) +} + +func ParseInt64(s string) int64 { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return int64(val) +} + +type ParseKV struct { + Key string + Value *interface{} + ArrayParser func(children []*JSONASTNode) + ObjectParser func(children []*JSONASTKV) + CustomParser func(node *JSONASTNode) +} + +func ParseAny(s string, val *interface{}) { + tokens := tokenize(s) + if len(tokens) == 0 { + panic("empty json") + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) > 0 { + panic("invalid json") + } + ast.ParseAny(val) +} + +func (ast *JSONASTNode) ParseObject(kv []*ParseKV) { + if ast.Kind != JSONKindObject { + panic("not an object") + } + for _, elem := range kv { + for i, child := range ast.ObjectChildren { + if child.Key == elem.Key { + if elem.ArrayParser != nil { + if child.Value.Kind != JSONKindArray { + panic("not an array") + } + elem.ArrayParser(child.Value.ArrayChildren) + } else if elem.ObjectParser != nil { + if child.Value.Kind != JSONKindObject { + panic("not an object") + } + elem.ObjectParser(child.Value.ObjectChildren) + } else if elem.CustomParser != nil { + elem.CustomParser(child.Value) + } else { + child.Value.ParseAny(elem.Value) + } + break + } + if i == (len(ast.ObjectChildren) - 1) { + panic("invalid key `" + elem.Key + "` in object `" + ast.String() + "`") + } + } + } +} + +func ParseSlice(s string) []*JSONASTNode { + ast := TokenizeAndParse(s) + return ast.ParseSlice() +} + +func (ast *JSONASTNode) ParseSlice() []*JSONASTNode { + if ast.Kind != JSONKindArray { + panic("not an array") + } + return ast.ArrayChildren +} + +func countWhitespaces(s string) int { + i := 0 + for i < len(s) { + if strings.ContainsRune(whitespaces, int32(s[i])) { + i++ + } else { + break + } + } + return i +} + +func JSONTokensString(tokens []*JSONToken) string { + s := "" + for _, token := range tokens { + s += token.Raw + } + return s +} + +func (node *JSONASTNode) String() string { + if node == nil { + return "nil" + } + switch node.Kind { + case JSONKindValue: + return node.Value + case JSONKindArray: + s := "[" + for i, child := range node.ArrayChildren { + if i > 0 { + s += "," + } + s += child.String() + } + s += "]" + return s + case JSONKindObject: + s := "{" + for i, child := range node.ObjectChildren { + if i > 0 { + s += "," + } + s += `"` + child.Key + `":` + child.Value.String() + } + s += "}" + return s + default: + panic("invalid json") + } +} + +func TokenizeAndParse(s string) *JSONASTNode { + tokens := tokenize(s) + if len(tokens) == 0 { + panic("empty json") + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) > 0 { + panic("invalid json") + } + return ast +} + +func parseAST(tokens []*JSONToken) (tkn []*JSONToken, tree *JSONASTNode) { + if len(tokens) == 0 { + panic("empty json") + } + + switch tokens[0].Kind { + + case JSONTokenKindString: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindNumber: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindTrue: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindFalse: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindNull: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + + case JSONTokenKindOpenArray: + arrayChildren := []*JSONASTNode{} + tokens = tokens[1:] + for len(tokens) > 0 { + if tokens[0].Kind == JSONTokenKindCloseArray { + return tokens[1:], &JSONASTNode{Kind: JSONKindArray, ArrayChildren: arrayChildren} + } + var child *JSONASTNode + tokens, child = parseAST(tokens) + arrayChildren = append(arrayChildren, child) + if len(tokens) == 0 { + panic("exepected more tokens in array") + } + if tokens[0].Kind == JSONTokenKindComma { + tokens = tokens[1:] + } else if tokens[0].Kind == JSONTokenKindCloseArray { + return tokens[1:], &JSONASTNode{Kind: JSONKindArray, ArrayChildren: arrayChildren} + } else { + panic("unexpected token in array after value `" + tokens[0].Raw + "`") + } + } + + case JSONTokenKindOpenObject: + objectChildren := []*JSONASTKV{} + if len(tokens) < 2 { + panic("objects must have at least 2 tokens") + } + tokens = tokens[1:] + for len(tokens) > 0 { + if tokens[0].Kind == JSONTokenKindCloseObject { + return tokens[1:], &JSONASTNode{Kind: JSONKindObject, ObjectChildren: objectChildren} + } + if tokens[0].Kind != JSONTokenKindString { + panic("invalid json") + } + key := tokens[0].Raw + tokens = tokens[1:] + if len(tokens) == 0 { + panic("exepected more tokens in object") + } + if tokens[0].Kind != JSONTokenKindColon { + panic("expected :") + } + tokens = tokens[1:] + if len(tokens) == 0 { + panic("exepected more tokens in object after :") + } + var value *JSONASTNode + tokens, value = parseAST(tokens) + objectChildren = append(objectChildren, &JSONASTKV{Key: ParseString(key), Value: value}) + if len(tokens) == 0 { + panic("exepected more tokens in object after value") + } + if tokens[0].Kind == JSONTokenKindComma { + tokens = tokens[1:] + } else if tokens[0].Kind == JSONTokenKindCloseObject { + return tokens[1:], &JSONASTNode{Kind: JSONKindObject, ObjectChildren: objectChildren} + } else { + panic("unexpected token in object after value `" + tokens[0].Raw + "`") + } + } + + default: + panic("unexpected token `" + tokens[0].Raw + "`") + } +} + +func tokenize(s string) []*JSONToken { + tokens := []*JSONToken{} + for len(s) > 0 { + var token *JSONToken + s, token = tokenizeOne(s) + if token.Kind != JSONTokenKindSpaces { + tokens = append(tokens, token) + } + } + return tokens +} + +func (node *JSONASTNode) ParseAVLTree(t *interface{}) *avl.Tree { + if node.Kind != JSONKindObject { + panic("not an object") + } + tree := avl.NewTree() + for _, child := range node.ObjectChildren { + child.Value.ParseAny(t) + tree.Set(child.Key, *t) + } + return tree +} + +func ParseAVLTree(s string, t *interface{}) *avl.Tree { + return TokenizeAndParse(s).ParseAVLTree(t) +} + +func tokenizeOne(s string) (string, *JSONToken) { + if len(s) == 0 { + panic("invalid token") + } + spacesCount := countWhitespaces(s) + if spacesCount > 0 { + spaces := s[:spacesCount] + return s[spacesCount:], &JSONToken{Kind: JSONTokenKindSpaces, Raw: spaces} + } + switch s[0] { + case '"': + return parseStringToken(s) + case 't': + return parseKeyword(s, "true", JSONTokenKindTrue) + case 'f': + return parseKeyword(s, "false", JSONTokenKindFalse) + case 'n': + return parseKeyword(s, "null", JSONTokenKindNull) + case '{': + return s[1:], &JSONToken{Kind: JSONTokenKindOpenObject, Raw: "{"} + case '[': + return s[1:], &JSONToken{Kind: JSONTokenKindOpenArray, Raw: "["} + case ':': + return s[1:], &JSONToken{Kind: JSONTokenKindColon, Raw: ":"} + case ',': + return s[1:], &JSONToken{Kind: JSONTokenKindComma, Raw: ","} + case ']': + return s[1:], &JSONToken{Kind: JSONTokenKindCloseArray, Raw: "]"} + case '}': + return s[1:], &JSONToken{Kind: JSONTokenKindCloseObject, Raw: "}"} + default: + return parseNumber(s) + } +} + +func parseKeyword(s string, keyword string, kind JSONTokenKind) (string, *JSONToken) { + if len(s) < len(keyword) { + panic("invalid keyword") + } + if s[:len(keyword)] != keyword { + panic("invalid keyword") + } + return s[len(keyword):], &JSONToken{Kind: kind, Raw: keyword} +} + +func parseStringToken(s string) (string, *JSONToken) { + if (len(s) < 2) || (s[0] != '"') { + panic("invalid string") + } + quote := false + for i := 1; i < len(s); i++ { + if !quote && s[i] == '\\' { + quote = true + continue + } + if !quote && s[i] == '"' { + return s[i+1:], &JSONToken{Kind: JSONTokenKindString, Raw: s[:i+1]} + } + quote = false + } + panic("invalid string") +} + +// copiloted +func parseNumber(s string) (string, *JSONToken) { + if len(s) == 0 { + panic("invalid number") + } + i := 0 + if s[i] == '-' { + i++ + } + if i == len(s) { + panic("invalid number") + } + if s[i] == '0' { + i++ + } else if ('1' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + if i == len(s) { + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s} + } + if s[i] == '.' { + i++ + if i == len(s) { + panic("invalid number") + } + if ('0' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + } + if i == len(s) { + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s} + } + if (s[i] == 'e') || (s[i] == 'E') { + i++ + if i == len(s) { + panic("invalid number") + } + if (s[i] == '+') || (s[i] == '-') { + i++ + } + if i == len(s) { + panic("invalid number") + } + if ('0' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + } + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s[:i]} +} + +type JSONTokenKind int + +type JSONKind int + +const ( + JSONKindUnknown JSONKind = iota + JSONKindValue + JSONKindObject + JSONKindArray +) + +type JSONASTNode struct { + Kind JSONKind + ArrayChildren []*JSONASTNode + ObjectChildren []*JSONASTKV + ValueKind JSONTokenKind + Value string +} + +type JSONASTKV struct { + Key string + Value *JSONASTNode +} + +const ( + JSONTokenKindUnknown JSONTokenKind = iota + JSONTokenKindString + JSONTokenKindNumber + JSONTokenKindTrue + JSONTokenKindFalse + JSONTokenKindSpaces + JSONTokenKindComma + JSONTokenKindColon + JSONTokenKindOpenArray + JSONTokenKindCloseArray + JSONTokenKindOpenObject + JSONTokenKindCloseObject + JSONTokenKindNull +) + +func (k JSONTokenKind) String() string { + switch k { + case JSONTokenKindString: + return "string" + case JSONTokenKindNumber: + return "number" + case JSONTokenKindTrue: + return "true" + case JSONTokenKindFalse: + return "false" + case JSONTokenKindSpaces: + return "spaces" + case JSONTokenKindComma: + return "comma" + case JSONTokenKindColon: + return "colon" + case JSONTokenKindOpenArray: + return "open-array" + case JSONTokenKindCloseArray: + return "close-array" + case JSONTokenKindOpenObject: + return "open-object" + case JSONTokenKindCloseObject: + return "close-object" + case JSONTokenKindNull: + return "null" + default: + return "unknown" + } +} + +type JSONToken struct { + Kind JSONTokenKind + Raw string +} diff --git a/examples/gno.land/p/demo/ujson/strings.gno b/examples/gno.land/p/demo/ujson/strings.gno new file mode 100644 index 00000000000..b4781a7de38 --- /dev/null +++ b/examples/gno.land/p/demo/ujson/strings.gno @@ -0,0 +1,233 @@ +package ujson + +import ( + "unicode/utf8" + + "gno.land/p/demo/utf16" +) + +const ( + ReplacementChar = '\uFFFD' // Represents invalid code points. +) + +// Ported from https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/encode.go +func FormatString(s string) string { + const escapeHTML = true + e := `"` // e.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) { + i++ + continue + } + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += "\\" // e.WriteByte('\\') + switch b { + case '\\', '"': + e += string(b) // e.WriteByte(b) + case '\n': + e += "n" // e.WriteByte('n') + case '\r': + e += "r" // e.WriteByte('r') + case '\t': + e += "t" // e.WriteByte('t') + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + // If escapeHTML is set, it also escapes <, >, and & + // because they can lead to security holes when + // user-controlled strings are rendered into JSON + // and served to some browsers. + e += `u00` // e.WriteString(`u00`) + e += string(hex[b>>4]) // e.WriteByte(hex[b>>4]) + e += string(hex[b&0xF]) // e.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\ufffd` // e.WriteString(`\ufffd`) + i += size + start = i + continue + } + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + if c == '\u2028' || c == '\u2029' { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\u202` // e.WriteString(`\u202`) + e += string(hex[c&0xF]) // e.WriteByte(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + e += s[start:] // e.WriteString(s[start:]) + } + e += `"` // e.WriteByte('"') + return e +} + +// Ported from https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/decode.go +// unquote converts a quoted JSON string literal s into an actual string t. +// The rules are different than for Go, so cannot use strconv.Unquote. +func ParseString(s string) string { + o, ok := unquoteBytes([]byte(s)) + if !ok { + panic("invalid string") + } + return string(o) +} + +func unquoteBytes(s []byte) (t []byte, ok bool) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return + } + s = s[1 : len(s)-1] + + // Check for unusual characters. If there are none, + // then no unquoting is needed, so return a slice of the + // original bytes. + r := 0 + for r < len(s) { + c := s[r] + if c == '\\' || c == '"' || c < ' ' { + break + } + if c < utf8.RuneSelf { + r++ + continue + } + rr, size := utf8.DecodeRune(s[r:]) + if rr == utf8.RuneError && size == 1 { + break + } + r += size + } + if r == len(s) { + return s, true + } + + b := make([]byte, len(s)+2*utf8.UTFMax) + w := copy(b, s[0:r]) + for r < len(s) { + // Out of room? Can only happen if s is full of + // malformed UTF-8 and we're replacing each + // byte with RuneError. + if w >= len(b)-2*utf8.UTFMax { + nb := make([]byte, (len(b)+utf8.UTFMax)*2) + copy(nb, b[0:w]) + b = nb + } + switch c := s[r]; { + case c == '\\': + r++ + if r >= len(s) { + return + } + switch s[r] { + default: + return + case '"', '\\', '/', '\'': + b[w] = s[r] + r++ + w++ + case 'b': + b[w] = '\b' + r++ + w++ + case 'f': + b[w] = '\f' + r++ + w++ + case 'n': + b[w] = '\n' + r++ + w++ + case 'r': + b[w] = '\r' + r++ + w++ + case 't': + b[w] = '\t' + r++ + w++ + case 'u': + r-- + rr := getu4(s[r:]) + if rr < 0 { + return + } + r += 6 + if utf16.IsSurrogate(rr) { + rr1 := getu4(s[r:]) + if dec := utf16.DecodeRune(rr, rr1); dec != ReplacementChar { + // A valid pair; consume. + r += 6 + w += utf8.EncodeRune(b[w:], dec) + break + } + // Invalid surrogate; fall back to replacement rune. + rr = ReplacementChar + } + + } + + // Quote, control characters are invalid. + case c == '"', c < ' ': + return + + // ASCII + case c < utf8.RuneSelf: + b[w] = c + r++ + w++ + + // Coerce to well-formed UTF-8. + default: + rr, size := utf8.DecodeRune(s[r:]) + r += size + w += utf8.EncodeRune(b[w:], rr) + } + } + return b[0:w], true +} + +// getu4 decodes \uXXXX from the beginning of s, returning the hex value, +// or it returns -1. +func getu4(s []byte) rune { + if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { + return -1 + } + var r rune + for _, c := range s[2:6] { + switch { + case '0' <= c && c <= '9': + c = c - '0' + case 'a' <= c && c <= 'f': + c = c - 'a' + 10 + case 'A' <= c && c <= 'F': + c = c - 'A' + 10 + default: + return -1 + } + r = r*16 + rune(c) + } + return r +} diff --git a/examples/gno.land/p/demo/ujson/tables.gno b/examples/gno.land/p/demo/ujson/tables.gno new file mode 100644 index 00000000000..1ec2db8d917 --- /dev/null +++ b/examples/gno.land/p/demo/ujson/tables.gno @@ -0,0 +1,216 @@ +package ujson + +import "unicode/utf8" + +var hex = "0123456789abcdef" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML